Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ semantic-version = "2.1.1"
junit-jupiter = "5.10.0"
logback = "1.5.19"
mockwebserver = "4.12.0"
mockito = "4.8.0"
mockito = "5.12.0"
android-retrofuture = "1.7.4"
android-gradle = "8.0.0"
android-gradle = "8.2.0"

[libraries]
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" }
Expand All @@ -21,7 +21,7 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" }
mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" }
mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito" }
mockito-inline = { module = "org.mockito:mockito-core", version.ref = "mockito" }
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-jupiter" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" }
android-retrofuture = { module = "net.sourceforge.streamsupport:android-retrofuture", version.ref = "android-retrofuture" }
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Thu Aug 04 20:00:01 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
52 changes: 31 additions & 21 deletions src/main/java/com/configcat/ConfigCatClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@

private static <T> void validateReturnType(Class<T> classOfT) {
if (!(classOfT == String.class || classOfT == Integer.class || classOfT == int.class || classOfT == Double.class || classOfT == double.class || classOfT == Boolean.class || classOfT == boolean.class)) {
throw new IllegalArgumentException("Only String, Integer, Double or Boolean types are supported.");
throw new EvaluationException("Only String, Integer, Double or Boolean types are supported.");
}
}

Expand Down Expand Up @@ -121,19 +121,16 @@
public <T> EvaluationDetails<T> getValueDetails(Class<T> classOfT, String key, User user, T defaultValue) {
if (key == null || key.isEmpty())
throw new IllegalArgumentException("'key' cannot be null or empty.");

validateReturnType(classOfT);

try {
return this.getValueDetailsAsync(classOfT, key, user, defaultValue).get();
} catch (InterruptedException e) {
String error = "Thread interrupted.";
this.logger.error(0, error, e);
Thread.currentThread().interrupt();
return EvaluationDetails.fromError(key, defaultValue, error + ": " + e.getMessage(), user);
return EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR,error + ": " + e.getMessage(), e, user);
} catch (Exception e) {
this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e);

Check failure on line 132 in src/main/java/com/configcat/ConfigCatClient.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "getValueDetails" 4 times.

See more on https://sonarcloud.io/project/issues?id=configcat_android-sdk&issues=AZ9mnm_Au7BfpSjcxycU&open=AZ9mnm_Au7BfpSjcxycU&pullRequest=82
return EvaluationDetails.fromError(key, defaultValue, e.getMessage(), user);
return EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR, e.getMessage(), e, user);
}
}

Expand All @@ -147,19 +144,30 @@
if (key == null || key.isEmpty())
throw new IllegalArgumentException("'key' cannot be null or empty.");

validateReturnType(classOfT);
try {
validateReturnType(classOfT);

return this.getSettingsAsync()
return this.getSettingsAsync()
.thenApply(settingsResult -> {
Result<Setting> checkSettingResult = checkSettingAvailable(settingsResult, key, defaultValue);
Result<Setting, EvaluationErrorCode> checkSettingResult = checkSettingAvailable(settingsResult, key, defaultValue);
if (checkSettingResult.error() != null) {
EvaluationDetails<Object> evaluationDetails = EvaluationDetails.fromError(key, defaultValue, checkSettingResult.error(), user);
EvaluationDetails<Object> evaluationDetails = EvaluationDetails.fromError(key, defaultValue, checkSettingResult.errorCode(), checkSettingResult.error(), null, user);
this.hooks.invokeOnFlagEvaluated(evaluationDetails);
return evaluationDetails.asTypeSpecific();
}
return this.evaluate(classOfT, checkSettingResult.value(),
key, user != null ? user : this.defaultUser, settingsResult.fetchTime(), settingsResult.settings());
});
} catch (InvalidConfigModelException e) {
this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e);
return CompletableFuture.completedFuture(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.INVALID_CONFIG_MODEL, e.getMessage(), null, user));
} catch (EvaluationException e) {
this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e);
return CompletableFuture.completedFuture(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.SETTING_VALUE_TYPE_MISMATCH, e.getMessage(),null, user));
} catch (Exception e) {
this.logger.error(1002, ConfigCatLogMessages.getSettingEvaluationErrorWithDefaultValue("getValueDetails", key, "defaultValue", defaultValue), e);
return CompletableFuture.completedFuture(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR, e.getMessage(), e, user));
}
}

@Override
Expand Down Expand Up @@ -331,14 +339,14 @@
} catch (Exception e) {
this.logger.error(1003, ConfigCatLogMessages.getForceRefreshError("forceRefresh"), e);
}
return new RefreshResult(false, "An error occurred during the refresh.");
return new RefreshResult(false, "An error occurred during the refresh.", RefreshErrorCode.UNEXPECTED_ERROR, null);
}

@Override
public CompletableFuture<RefreshResult> forceRefreshAsync() {
if (configService == null) {
return CompletableFuture.completedFuture(new RefreshResult(false,
"The ConfigCat SDK is in local-only mode. Calling .forceRefresh() has no effect."));
"The ConfigCat SDK is in local-only mode. Calling .forceRefresh() has no effect.", RefreshErrorCode.LOCAL_ONLY_CLIENT, null));
}

return configService.refresh();
Expand Down Expand Up @@ -447,15 +455,15 @@
private <T> T getValueFromSettingsMap(Class<T> classOfT, SettingResult settingResult, String key, User user, T defaultValue) {
User userObject = user != null ? user : this.defaultUser;
try {
Result<Setting> checkSettingResult = checkSettingAvailable(settingResult, key, defaultValue);
Result<Setting, EvaluationErrorCode> checkSettingResult = checkSettingAvailable(settingResult, key, defaultValue);
if (checkSettingResult.error() != null) {
this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, checkSettingResult.error(), user));
this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, checkSettingResult.errorCode(), checkSettingResult.error(), null, user));
return defaultValue;
}
return this.evaluate(classOfT, checkSettingResult.value(), key, userObject, settingResult.fetchTime(), settingResult.settings()).getValue();
} catch (Exception | NoSuchMethodError e) {
FormattableLogMessage error = ConfigCatLogMessages.getSettingEvaluationFailedForOtherReason(key, "defaultValue", defaultValue);
this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, error + " " + e.getMessage(), userObject));
this.hooks.invokeOnFlagEvaluated(EvaluationDetails.fromError(key, defaultValue, EvaluationErrorCode.UNEXPECTED_ERROR, error + " " + e.getMessage(), e, userObject));
this.logger.error(2001, error, e);
return defaultValue;
}
Expand Down Expand Up @@ -518,6 +526,8 @@
user,
false,
null,
EvaluationErrorCode.NONE,
null,
fetchTime,
evaluationResult.targetingRule,
evaluationResult.percentageOption);
Expand All @@ -537,7 +547,7 @@
else if ((classOfT == Boolean.class || classOfT == boolean.class) && settingValue.getBooleanValue() != null && SettingType.BOOLEAN.equals(settingType))
return settingValue.getBooleanValue();

throw new IllegalArgumentException("The type of a setting must match the type of the specified default value. "
throw new EvaluationException("The type of a setting must match the type of the specified default value. "
+ "Setting's type was {" + settingType + "} but the default value's type was {" + classOfT + "}. "
+ "Please use a default value which corresponds to the setting type {" + settingType + "}."
+ "Learn more: https://configcat.com/docs/sdk-reference/android/#setting-type-mapping");
Expand All @@ -553,7 +563,7 @@
else if (settingType == SettingType.DOUBLE)
return double.class;
else
throw new IllegalArgumentException("Only String, Integer, Double or Boolean types are supported");
throw new EvaluationException("Only String, Integer, Double or Boolean types are supported");
}

private boolean checkSettingsAvailable(SettingResult settingResult, String emptyResult) {
Expand All @@ -565,22 +575,22 @@
return true;
}

private <T> Result<Setting> checkSettingAvailable(SettingResult settingResult, String key, T defaultValue) {
private <T> Result<Setting, EvaluationErrorCode> checkSettingAvailable(SettingResult settingResult, String key, T defaultValue) {
if (settingResult.isEmpty()) {
FormattableLogMessage errorMessage = ConfigCatLogMessages.getConfigJsonIsNotPresentedWithDefaultValue(key, "defaultValue", defaultValue);
this.logger.error(1000, errorMessage);
return Result.error(errorMessage, null);
return Result.error(errorMessage, null, EvaluationErrorCode.CONFIG_JSON_NOT_AVAILABLE, null);
}

Map<String, Setting> settings = settingResult.settings();
Setting setting = settings.get(key);
if (setting == null) {
FormattableLogMessage errorMessage = ConfigCatLogMessages.getSettingEvaluationFailedDueToMissingKey(key, "defaultValue", defaultValue, settings.keySet());
this.logger.error(1001, errorMessage);
return Result.error(errorMessage, null);
return Result.error(errorMessage, null, EvaluationErrorCode.SETTING_KEY_MISSING, null);
}

return Result.success(setting);
return Result.success(setting, EvaluationErrorCode.NONE);
}

/**
Expand Down
38 changes: 23 additions & 15 deletions src/main/java/com/configcat/ConfigFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public enum Status {
private final Status status;
private final Entry entry;
private final Object error;
private final RefreshErrorCode errorCode;
private final Throwable errorException;
private final boolean fetchTimeUpdatable;
private final String cfRayId;

Expand Down Expand Up @@ -49,26 +51,32 @@ public Object error() {
return this.error;
}

public RefreshErrorCode errorCode() {return this.errorCode;}

public Throwable errorException() {return this.errorException;}

public String cfRayId() {return this.cfRayId;}

FetchResponse(Status status, Entry entry, Object error, boolean fetchTimeUpdatable, String cfRayId) {
FetchResponse(Status status, Entry entry, Object error, RefreshErrorCode errorCode,Throwable errorException, boolean fetchTimeUpdatable, String cfRayId) {
this.status = status;
this.entry = entry;
this.error = error;
this.errorCode = errorCode;
this.errorException = errorException;
this.fetchTimeUpdatable = fetchTimeUpdatable;
this.cfRayId = cfRayId;
}

public static FetchResponse fetched(Entry entry, String cfRayId) {
return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, false, cfRayId);
return new FetchResponse(Status.FETCHED, entry == null ? Entry.EMPTY : entry, null, RefreshErrorCode.NONE, null, false, cfRayId);
}

public static FetchResponse notModified(String cfRayId) {
return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, true, cfRayId);
return new FetchResponse(Status.NOT_MODIFIED, Entry.EMPTY, null, RefreshErrorCode.NONE, null, true, cfRayId);
}

public static FetchResponse failed(Object error, boolean fetchTimeUpdatable, String cfRayId) {
return new FetchResponse(Status.FAILED, Entry.EMPTY, error, fetchTimeUpdatable, cfRayId);
public static FetchResponse failed(Object error, RefreshErrorCode errorCode, Throwable errorException, boolean fetchTimeUpdatable, String cfRayId) {
return new FetchResponse(Status.FAILED, Entry.EMPTY, error,errorCode, errorException, fetchTimeUpdatable, cfRayId);
}
}

Expand Down Expand Up @@ -190,9 +198,9 @@ private void callHTTP(String previousETag, CompletableFuture<FetchResponse> resu
if (responseCode == 200) {
String content = readBody(urlConnection.getInputStream());
String eTag = readHeaderValue(responseHeaders,"ETag");
Result<Config> configResult = deserializeConfig(content, cfRayId);
Result<Config, EvaluationErrorCode> configResult = deserializeConfig(content, cfRayId);
if (configResult.error() != null) {
fetchResponse = FetchResponse.failed(configResult.error(), false, cfRayId);
fetchResponse = FetchResponse.failed(configResult.error(), RefreshErrorCode.INVALID_HTTP_RESPONSE_CONTENT, null, false, cfRayId);
} else {
logger.debug("Fetch was successful: new config fetched.");
fetchResponse = FetchResponse.fetched(new Entry(configResult.value(), eTag, content, System.currentTimeMillis()), cfRayId);
Expand All @@ -207,24 +215,24 @@ private void callHTTP(String previousETag, CompletableFuture<FetchResponse> resu
} else if (responseCode == 403 || responseCode == 404) {
FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToInvalidSDKKey(cfRayId);
logger.error(1100, message);
fetchResponse = FetchResponse.failed(message, true, cfRayId);
fetchResponse = FetchResponse.failed(message, RefreshErrorCode.INVALID_SDK_KEY, null, true, cfRayId);
} else {
FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToUnexpectedHttpResponse(responseCode, urlConnection.getResponseMessage(), cfRayId);
logger.error(1101, message);
fetchResponse = FetchResponse.failed(message, false, cfRayId);
fetchResponse = FetchResponse.failed(message, RefreshErrorCode.UNEXPECTED_HTTP_RESPONSE, null,false, cfRayId);
}

} catch (SocketTimeoutException e) {
FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToRequestTimeout(httpOptions.getConnectTimeoutMillis(), httpOptions.getReadTimeoutMillis(), cfRayId);
logger.error(1102, message, e);
fetchResponse = FetchResponse.failed(message, false, cfRayId);
fetchResponse = FetchResponse.failed(message, RefreshErrorCode.HTTP_REQUEST_TIMEOUT, e, false, cfRayId);
} catch (Exception e) {
FormattableLogMessage message = ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId);
logger.error(1103, message, e);
fetchResponse = FetchResponse.failed(message + " " + e.getMessage(), false, cfRayId);
fetchResponse = FetchResponse.failed(message, RefreshErrorCode.HTTP_REQUEST_FAILURE, e, false, cfRayId);
} finally {
if(fetchResponse == null) {
fetchResponse = FetchResponse.failed(ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId), false, cfRayId);
fetchResponse = FetchResponse.failed(ConfigCatLogMessages.getFetchFailedDueToUnexpectedError(cfRayId), RefreshErrorCode.UNEXPECTED_ERROR, null, false, cfRayId);
}
result.complete(fetchResponse);
if (urlConnection != null) {
Expand Down Expand Up @@ -271,13 +279,13 @@ private String readBody(InputStream inputStream) throws IOException {
return body.toString();
}

private Result<Config> deserializeConfig(String json, String cfRayId) {
private Result<Config, EvaluationErrorCode> deserializeConfig(String json, String cfRayId) {
try {
return Result.success(Utils.deserializeConfig(json));
return Result.success(Utils.deserializeConfig(json), EvaluationErrorCode.NONE);
} catch (Exception e) {
FormattableLogMessage message = ConfigCatLogMessages.getFetchReceived200WithInvalidBodyError(cfRayId);
this.logger.error(1105, message, e);
return Result.error(message, null);
return Result.error(message, null, EvaluationErrorCode.INVALID_CONFIG_MODEL, null);
}
}
}
Expand Down
Loading