Skip to content

Commit

Permalink
Add data governance related functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
z4kn4fein committed Sep 23, 2020
1 parent f24601d commit 0aae426
Show file tree
Hide file tree
Showing 28 changed files with 445 additions and 130 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2018 ConfigCat
Copyright (c) 2020 ConfigCat

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ ConfigCat is a <a href="https://configcat.com" target="_blank">hosted feature fl
### 1. Install the package
*Gradle:*
```groovy
implementation 'com.configcat:configcat-android-client:5.+'
implementation 'com.configcat:configcat-android-client:6.+'
```

### 2. Go to <a href="https://app.configcat.com/sdkkey" target="_blank">Connect your application</a> tab to get your *SDK Key*:
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=5.1.1
version=6.0.0
2 changes: 1 addition & 1 deletion samples/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation group: "com.configcat", name: "configcat-android-client", version:"5.+"
implementation group: "com.configcat", name: "configcat-android-client", version:"6.+"
implementation 'org.slf4j:slf4j-android:1.+'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/configcat/AutoPollingMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* The auto polling mode configuration.
*/
public class AutoPollingMode extends PollingMode {
private int autoPollRateInSeconds;
private ConfigurationChangeListener listener;
private final int autoPollRateInSeconds;
private final ConfigurationChangeListener listener;

AutoPollingMode(int autoPollRateInSeconds, ConfigurationChangeListener listener) {
if(autoPollRateInSeconds < 2)
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/configcat/AutoPollingPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ class AutoPollingPolicy extends RefreshPolicy {
this.scheduler.scheduleAtFixedRate(() -> {
try {
FetchResponse response = super.fetcher().getConfigurationJsonStringAsync().get();
String cached = super.cache().get();
String cached = super.readConfigCache();
String config = response.config();
if (response.isFetched() && !config.equals(cached)) {
super.cache().set(config);
super.writeConfigCache(config);
this.broadcastConfigurationChanged();
}

Expand All @@ -60,9 +60,9 @@ class AutoPollingPolicy extends RefreshPolicy {
@Override
public CompletableFuture<String> getConfigurationJsonAsync() {
if(this.initFuture.isDone())
return CompletableFuture.completedFuture(super.cache().get());
return CompletableFuture.completedFuture(super.readConfigCache());

return this.initFuture.thenApplyAsync(v -> super.cache().get());
return this.initFuture.thenApplyAsync(v -> super.readConfigCache());
}

@Override
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/configcat/ConfigAttributes.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
package com.configcat;

class Config {
static final String Preferences = "p";
static final String Entries = "f";
}

class Preferences {
static final String BaseUrl = "u";
static final String Redirect = "r";
}

class Setting {
static final String Value = "v";
static final String Type = "t";
Expand Down
42 changes: 4 additions & 38 deletions src/main/java/com/configcat/ConfigCache.java
Original file line number Diff line number Diff line change
@@ -1,61 +1,27 @@
package com.configcat;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;

/**
* A cache API used to make custom cache implementations for {@link ConfigCatClient}.
*/
public abstract class ConfigCache {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigCache.class);
private String inMemoryValue;

public String get() {
try {
return this.read();
} catch (Exception e) {
LOGGER.error("An error occurred during the cache read", e);
return this.inMemoryValue;
}
}

public void set(String value) {
try {
this.inMemoryValue = value;
this.write(value);
} catch (Exception e) {
LOGGER.error("An error occurred during the cache write", e);
}
}

/**
* Through this getter, the in-memory representation of the cached value can be accessed.
* When the underlying cache implementations is not able to load or store its value,
* this will represent the latest cached configuration.
*
* @return the cached value in memory.
*/
public String inMemoryValue() { return this.inMemoryValue; }

/**
* Child classes has to implement this method, the {@link ConfigCatClient}
* uses it to get the actual value from the cache.
*
* @param key the key of the cache entry.
* @return the cached configuration.
* @throws Exception if unable to read the cache.
*/
protected abstract String read() throws Exception;
protected abstract String read(String key) throws Exception;

/**
* * Child classes has to implement this method, the {@link ConfigCatClient}
* uses it to set the actual cached value.
*
* @param key the key of the cache entry.
* @param value the new value to cache.
* @throws Exception if unable to save the value.
*/
protected abstract void write(String value) throws Exception;
protected abstract void write(String key, String value) throws Exception;
}

25 changes: 23 additions & 2 deletions src/main/java/com/configcat/ConfigCatClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
public final class ConfigCatClient implements ConfigurationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigCatClient.class);
private static final ConfigurationParser parser = new ConfigurationParser();
private static final String BASE_URL_GLOBAL = "https://cdn-global.configcat.com";
private static final String BASE_URL_EU = "https://cdn-eu.configcat.com";

private final RefreshPolicy refreshPolicy;
private final int maxWaitTimeForSyncCallsInSeconds;

Expand All @@ -31,15 +34,21 @@ private ConfigCatClient(String sdkKey, Builder builder) throws IllegalArgumentEx
? PollingModes.AutoPoll(60)
: builder.pollingMode;

boolean hasCustomBaseUrl = builder.baseUrl != null && !builder.baseUrl.isEmpty();
ConfigFetcher fetcher = new ConfigFetcher(builder.httpClient == null
? new OkHttpClient
.Builder()
.retryOnConnectionFailure(true)
.build()
: builder.httpClient,
sdkKey,
builder.baseUrl,
pollingMode);
!hasCustomBaseUrl
? builder.dataGovernance == DataGovernance.GLOBAL
? BASE_URL_GLOBAL
: BASE_URL_EU
: builder.baseUrl,
hasCustomBaseUrl,
pollingMode.getPollingIdentifier());

ConfigCache cache = builder.cache == null
? new InMemoryConfigCache()
Expand Down Expand Up @@ -300,6 +309,7 @@ public static class Builder {
private int maxWaitTimeForSyncCallsInSeconds;
private String baseUrl;
private PollingMode pollingMode;
private DataGovernance dataGovernance = DataGovernance.GLOBAL;

/**
* Sets the underlying http client which will be used to fetch the latest configuration.
Expand Down Expand Up @@ -345,6 +355,17 @@ public Builder mode(PollingMode pollingMode) {
return this;
}

/**
* Sets the preferred data governance.
*
* @param dataGovernance the {@link DataGovernance} parameter.
* @return the builder.
*/
public Builder dataGovernance(DataGovernance dataGovernance) {
this.dataGovernance = dataGovernance;
return this;
}

/**
* Sets the maximum time in seconds at most how long the synchronous calls
* e.g. {@code client.getConfiguration(...)} have to be blocked.
Expand Down
85 changes: 74 additions & 11 deletions src/main/java/com/configcat/ConfigFetcher.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.configcat;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -10,25 +12,85 @@

class ConfigFetcher implements Closeable {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigFetcher.class);
private static final String CONFIG_JSON_NAME = "config_v5.json";

private final JsonParser parser = new JsonParser();
private final OkHttpClient httpClient;
private final String url;
private final String mode;
private final String version;
private final String apiKey;
private final boolean urlIsCustom;
private String url;
private String eTag;

ConfigFetcher(OkHttpClient httpClient, String sdkKey, PollingMode mode) {
this(httpClient, sdkKey, null, mode);
}

ConfigFetcher(OkHttpClient httpClient, String sdkKey, String baseUrl, PollingMode mode) {
baseUrl = baseUrl == null || baseUrl.isEmpty() ? "https://cdn.configcat.com" : baseUrl;
ConfigFetcher(OkHttpClient httpClient,
String apiKey,
String url,
boolean urlIsCustom,
String pollingIdentifier) {
this.apiKey = apiKey;
this.urlIsCustom = urlIsCustom;
this.url = url;
this.httpClient = httpClient;
this.url = baseUrl + "/configuration-files/" + sdkKey + "/config_v4.json";
this.version = this.getClass().getPackage().getImplementationVersion();
this.mode = mode.getPollingIdentifier();
this.mode = pollingIdentifier;
}

public CompletableFuture<FetchResponse> getConfigurationJsonStringAsync() {
return this.executeFetchAsync(2);
}

private CompletableFuture<FetchResponse> executeFetchAsync(int executionCount) {
return this.getResponseAsync().thenComposeAsync(fetchResponse -> {
if(!fetchResponse.isFetched()) {
return CompletableFuture.completedFuture(fetchResponse);
}
try {
JsonObject json = parser.parse(fetchResponse.config()).getAsJsonObject();
JsonObject preferences = json.getAsJsonObject(Config.Preferences);
if(preferences == null) {
return CompletableFuture.completedFuture(fetchResponse);
}

String newUrl = preferences.get(Preferences.BaseUrl).getAsString();
if(newUrl == null || newUrl.isEmpty() || newUrl.equals(this.url)) {
return CompletableFuture.completedFuture(fetchResponse);
}

int redirect = preferences.get(Preferences.Redirect).getAsInt();

// we have a custom url set and we didn't get a forced redirect
if(this.urlIsCustom && redirect != 2) {
return CompletableFuture.completedFuture(fetchResponse);
}

this.url = newUrl;

if(redirect == 0) { // no redirect
return CompletableFuture.completedFuture(fetchResponse);
} else { // redirect
if (redirect == 1) {
LOGGER.warn("Please check the data_governance parameter in the ConfigCatClient initialization. " +
"It should match the settings provided in " +
"https://app.configcat.com/organization/data-governance. " +
"If you are not allowed to view this page, ask your Organization's Admins " +
"for the correct setting.");
}

if(executionCount > 0) {
return this.executeFetchAsync(executionCount - 1);
}
}

} catch (Exception exception) {
LOGGER.error("Exception in ConfigFetcher.executeFetchAsync", exception);
}

return CompletableFuture.completedFuture(fetchResponse);
});
}

private CompletableFuture<FetchResponse> getResponseAsync() {
Request request = this.getRequest();

CompletableFuture<FetchResponse> future = new CompletableFuture<>();
Expand All @@ -54,7 +116,7 @@ public void onResponse(Call call, Response response) {
future.complete(new FetchResponse(FetchResponse.Status.FAILED, null));
}
} catch (Exception e) {
LOGGER.error("Exception in ConfigFetcher.getConfigurationJsonStringAsync", e);
LOGGER.error("Exception in ConfigFetcher.getResponseAsync", e);
future.complete(new FetchResponse(FetchResponse.Status.FAILED, null));
}
}
Expand All @@ -79,13 +141,14 @@ public void close() throws IOException {
}

Request getRequest() {
String url = this.url + "/configuration-files/" + this.apiKey + "/" + CONFIG_JSON_NAME;
Request.Builder builder = new Request.Builder()
.addHeader("X-ConfigCat-UserAgent", "ConfigCat-Java/"+ this.mode + "-" + this.version);

if(this.eTag != null)
builder.addHeader("If-None-Match", this.eTag);

return builder.url(this.url).build();
return builder.url(url).build();
}
}

13 changes: 9 additions & 4 deletions src/main/java/com/configcat/ConfigurationParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public <T> T parseValue(Class<T> classOfT, String config, String key, User user)
public String parseVariationId(String config, String key, User user) throws ParsingFailedException {
try {
LOGGER.info("Evaluating getVariationId("+key+").");
JsonObject root = this.parser.parse(config).getAsJsonObject();
JsonObject root = this.parseConfigSection(config);

JsonObject node = root.getAsJsonObject(key);
if(node == null) {
Expand All @@ -61,7 +61,7 @@ public String parseVariationId(String config, String key, User user) throws Pars

public <T> Map.Entry<String, T> parseKeyValue(Class<T> classOfT, String config, String variationId) throws ParsingFailedException {
try {
Set<Map.Entry<String, JsonElement>> root = this.parser.parse(config).getAsJsonObject().entrySet();
Set<Map.Entry<String, JsonElement>> root = this.parseConfigSection(config).entrySet();
for (Map.Entry<String, JsonElement> node: root) {
String settingKey = node.getKey();
JsonObject setting = node.getValue().getAsJsonObject();
Expand Down Expand Up @@ -94,7 +94,7 @@ public <T> Map.Entry<String, T> parseKeyValue(Class<T> classOfT, String config,

public Collection<String> getAllKeys(String config) throws ParsingFailedException {
try {
JsonObject root = this.parser.parse(config).getAsJsonObject();
JsonObject root = this.parseConfigSection(config);
return root.keySet();

} catch (Exception e) {
Expand All @@ -105,7 +105,7 @@ public Collection<String> getAllKeys(String config) throws ParsingFailedExceptio
private Object parseValueInternal(Class<?> classOfT, String config, String key, User user) throws ParsingFailedException, IllegalArgumentException {
try {
LOGGER.info("Evaluating getValue("+key+").");
JsonObject root = this.parser.parse(config).getAsJsonObject();
JsonObject root = this.parseConfigSection(config);

JsonObject node = root.getAsJsonObject(key);
if(node == null) {
Expand All @@ -130,4 +130,9 @@ else if (classOfT == Double.class || classOfT == double.class)
else
return element.getAsBoolean();
}

private JsonObject parseConfigSection(String json) {
JsonObject root = this.parser.parse(json).getAsJsonObject();
return root.getAsJsonObject(Config.Entries);
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/configcat/DataGovernance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.configcat;

/**
* The available values for data governance.
*/
public enum DataGovernance {
GLOBAL,
EU_ONLY
}
Loading

0 comments on commit 0aae426

Please sign in to comment.