diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml
index 03fb4491..cacf90dd 100644
--- a/.github/workflows/merge.yml
+++ b/.github/workflows/merge.yml
@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
- name: Set up JDK 8
- uses: actions/setup-java@8f12c5c4d1ebd18bf4b7a51a1b0b912633f0bc61
+ uses: actions/setup-java@ea15b3b99cdc9ac45af1882d085e3f9297a75a8b
with:
java-version: '8'
distribution: 'temurin'
@@ -32,7 +32,7 @@ jobs:
server-password: ${{ secrets.OSSRH_PASSWORD }}
- name: Cache local Maven repository
- uses: actions/cache@2b5a782c6414f96354f95902141714b127692d1e
+ uses: actions/cache@6998d139ddd3e68c71e9e398d8e40b71a2f39812
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -49,7 +49,7 @@ jobs:
run: mvn --batch-mode --update-snapshots verify
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@e0fbd592d323cb2991fb586fdd260734fcb41fcb
+ uses: codecov/codecov-action@742000aae0f04cc2a6bf74c9934c84b579cc1c18
with:
flags: unittests # optional
name: coverage # optional
diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index fdefe2e1..62219ac5 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -19,19 +19,19 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
- name: Set up JDK 8
- uses: actions/setup-java@8f12c5c4d1ebd18bf4b7a51a1b0b912633f0bc61
+ uses: actions/setup-java@ea15b3b99cdc9ac45af1882d085e3f9297a75a8b
with:
java-version: '8'
distribution: 'temurin'
cache: maven
- name: Initialize CodeQL
- uses: github/codeql-action/init@a58e90a9daf2aac731400f56d005f2b324a2c5b8
+ uses: github/codeql-action/init@7ba5ed7eed12f15064a031cc1fa3341f93764020
with:
languages: java
- name: Cache local Maven repository
- uses: actions/cache@2b5a782c6414f96354f95902141714b127692d1e
+ uses: actions/cache@6998d139ddd3e68c71e9e398d8e40b71a2f39812
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -42,7 +42,7 @@ jobs:
run: mvn --batch-mode --update-snapshots verify -P integration-test
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@e0fbd592d323cb2991fb586fdd260734fcb41fcb
+ uses: codecov/codecov-action@742000aae0f04cc2a6bf74c9934c84b579cc1c18
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
flags: unittests # optional
@@ -51,4 +51,4 @@ jobs:
verbose: true # optional (default = false)
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@a58e90a9daf2aac731400f56d005f2b324a2c5b8
+ uses: github/codeql-action/analyze@7ba5ed7eed12f15064a031cc1fa3341f93764020
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 517b1be0..03d186c8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
- name: Set up JDK 8
if: ${{ steps.release.outputs.releases_created }}
- uses: actions/setup-java@8f12c5c4d1ebd18bf4b7a51a1b0b912633f0bc61
+ uses: actions/setup-java@ea15b3b99cdc9ac45af1882d085e3f9297a75a8b
with:
java-version: '8'
distribution: 'temurin'
diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml
index 42085389..70edc8e5 100644
--- a/.github/workflows/static-code-scanning.yaml
+++ b/.github/workflows/static-code-scanning.yaml
@@ -33,12 +33,12 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@a58e90a9daf2aac731400f56d005f2b324a2c5b8
+ uses: github/codeql-action/init@7ba5ed7eed12f15064a031cc1fa3341f93764020
with:
languages: java
- name: Autobuild
- uses: github/codeql-action/autobuild@a58e90a9daf2aac731400f56d005f2b324a2c5b8
+ uses: github/codeql-action/autobuild@7ba5ed7eed12f15064a031cc1fa3341f93764020
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@a58e90a9daf2aac731400f56d005f2b324a2c5b8
+ uses: github/codeql-action/analyze@7ba5ed7eed12f15064a031cc1fa3341f93764020
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 077bc02a..391149ac 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1 +1 @@
-{".":"1.1.0"}
\ No newline at end of file
+{".":"1.2.0"}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e42f88ca..51318976 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# Changelog
+## [1.2.0](https://github.com/open-feature/java-sdk/compare/v1.1.0...v1.2.0) (2023-02-10)
+
+
+### Features
+
+* added implementation of immutable evaluation context ([#210](https://github.com/open-feature/java-sdk/issues/210)) ([6c14d87](https://github.com/open-feature/java-sdk/commit/6c14d87c2e54c953eff351fa1ccdd914fa08b6ed))
+
+
+### Bug Fixes
+
+* **deps:** update dependency io.cucumber:cucumber-bom to v7.11.1 ([#271](https://github.com/open-feature/java-sdk/issues/271)) ([8845242](https://github.com/open-feature/java-sdk/commit/88452423303f8c1feada733f1c9d4db845d64a5b))
+* **deps:** update dependency org.projectlombok:lombok to v1.18.26 ([#277](https://github.com/open-feature/java-sdk/issues/277)) ([aad036a](https://github.com/open-feature/java-sdk/commit/aad036a0113e2d248e493d0d87e52320a66df7a2))
+* improve error logs for evaluation failure ([#276](https://github.com/open-feature/java-sdk/issues/276)) ([9349997](https://github.com/open-feature/java-sdk/commit/93499975d0b9ae30aa34db999d8aa3d7c955da70))
+* MutableContext and ImmutableContext merge are made recursive ([#280](https://github.com/open-feature/java-sdk/issues/280)) ([bd4e12e](https://github.com/open-feature/java-sdk/commit/bd4e12e16f3c4af5cdcad490977ccc0842e1ded6))
+
## [1.1.0](https://github.com/open-feature/java-sdk/compare/v1.0.1...v1.1.0) (2023-01-24)
diff --git a/README.md b/README.md
index 76c7883f..5ac1e6ea 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://maven-badges.herokuapp.com/maven-central/dev.openfeature/sdk)
[](https://javadoc.io/doc/dev.openfeature/sdk)
[](https://www.repostatus.org/#active)
-[](https://github.com/open-feature/spec/tree/v0.5.1)
+[](https://github.com/open-feature/spec/tree/v0.5.2)
[](https://snyk.io/test/github/open-feature/java-sdk)
[](https://github.com/open-feature/java-sdk/actions/workflows/merge.yml)
[](https://codecov.io/gh/open-feature/java-sdk)
@@ -68,7 +68,7 @@ For complete documentation, visit: https://docs.openfeature.dev/docs/category/co
dev.openfeature
sdk
- 1.1.0
+ 1.2.0
```
@@ -92,7 +92,7 @@ If you would like snapshot builds, this is the relevant repository information:
```groovy
dependencies {
- implementation 'dev.openfeature:sdk:1.1.0'
+ implementation 'dev.openfeature:sdk:1.2.0'
}
```
diff --git a/pom.xml b/pom.xml
index 83599d96..930d2b84 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
dev.openfeature
sdk
- 1.1.0
+ 1.2.0
UTF-8
@@ -44,7 +44,7 @@
org.projectlombok
lombok
- 1.18.24
+ 1.18.26
provided
@@ -59,7 +59,7 @@
org.slf4j
slf4j-api
- 1.7.36
+ 2.0.6
@@ -70,13 +70,6 @@
test
-
- uk.org.lidalia
- slf4j-test
- 1.2.0
- test
-
-
org.assertj
assertj-core
@@ -131,6 +124,13 @@
test
+
+ org.simplify4u
+ slf4j2-mock
+ 2.3.0
+ test
+
+
com.google.guava
guava
@@ -152,7 +152,7 @@
io.cucumber
cucumber-bom
- 7.11.0
+ 7.11.1
pom
import
@@ -228,6 +228,7 @@
com.github.spotbugs:*
org.junit*
+ org.simplify4u:slf4j2-mock*
com.google.guava*
@@ -235,7 +236,7 @@
org.junit*
com.google.code.findbugs*
com.github.spotbugs*
- uk.org.lidalia:lidalia-slf4j-ext:*
+ org.simplify4u:slf4j-mock-common:*
diff --git a/src/main/java/dev/openfeature/sdk/EvaluationContext.java b/src/main/java/dev/openfeature/sdk/EvaluationContext.java
index d613c700..10a7ea17 100644
--- a/src/main/java/dev/openfeature/sdk/EvaluationContext.java
+++ b/src/main/java/dev/openfeature/sdk/EvaluationContext.java
@@ -8,6 +8,10 @@
public interface EvaluationContext extends Structure {
String getTargetingKey();
+ /**
+ * Mutating targeting key is not supported in all implementations and will be removed.
+ */
+ @Deprecated
void setTargetingKey(String targetingKey);
/**
diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java
new file mode 100644
index 00000000..3fb25c28
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java
@@ -0,0 +1,96 @@
+package dev.openfeature.sdk;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import lombok.Getter;
+import lombok.ToString;
+import lombok.experimental.Delegate;
+
+/**
+ * The EvaluationContext is a container for arbitrary contextual data
+ * that can be used as a basis for dynamic evaluation.
+ * The ImmutableContext is an EvaluationContext implementation which is threadsafe, and whose attributes can
+ * not be modified after instantiation.
+ */
+@ToString
+@SuppressWarnings("PMD.BeanMembersShouldSerialize")
+public final class ImmutableContext implements EvaluationContext {
+
+ @Getter
+ private final String targetingKey;
+ @Delegate
+ private final Structure structure;
+
+ /**
+ * Create an immutable context with an empty targeting_key and attributes provided.
+ */
+ public ImmutableContext() {
+ this("", new HashMap<>());
+ }
+
+ /**
+ * Create an immutable context with given targeting_key provided.
+ *
+ * @param targetingKey targeting key
+ */
+ public ImmutableContext(String targetingKey) {
+ this(targetingKey, new HashMap<>());
+ }
+
+ /**
+ * Create an immutable context with an attributes provided.
+ *
+ * @param attributes evaluation context attributes
+ */
+ public ImmutableContext(Map attributes) {
+ this("", attributes);
+ }
+
+ /**
+ * Create an immutable context with given targetingKey and attributes provided.
+ *
+ * @param targetingKey targeting key
+ * @param attributes evaluation context attributes
+ */
+ public ImmutableContext(String targetingKey, Map attributes) {
+ this.structure = new ImmutableStructure(attributes);
+ this.targetingKey = targetingKey;
+ }
+
+ /**
+ * Mutating targeting key is not supported in ImmutableContext and will be removed.
+ */
+ @Override
+ @Deprecated
+ public void setTargetingKey(String targetingKey) {
+ throw new UnsupportedOperationException("changing of targeting key is not allowed");
+ }
+
+ /**
+ * Merges this EvaluationContext object with the passed EvaluationContext, overriding in case of conflict.
+ *
+ * @param overridingContext overriding context
+ * @return resulting merged context
+ */
+ @Override
+ public EvaluationContext merge(EvaluationContext overridingContext) {
+ if (overridingContext == null) {
+ return new ImmutableContext(this.targetingKey, this.asMap());
+ }
+ String newTargetingKey = "";
+ if (this.getTargetingKey() != null && !this.getTargetingKey().trim().equals("")) {
+ newTargetingKey = this.getTargetingKey();
+ }
+
+ if (overridingContext.getTargetingKey() != null && !overridingContext.getTargetingKey().trim().equals("")) {
+ newTargetingKey = overridingContext.getTargetingKey();
+ }
+
+ Map merged = this.merge(m -> new ImmutableStructure(m),
+ this.asMap(),
+ overridingContext.asMap());
+
+ return new ImmutableContext(newTargetingKey, merged);
+ }
+}
diff --git a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java
new file mode 100644
index 00000000..2dc2cdaf
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java
@@ -0,0 +1,87 @@
+package dev.openfeature.sdk;
+
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * {@link ImmutableStructure} represents a potentially nested object type which is used to represent
+ * structured data.
+ * The ImmutableStructure is a Structure implementation which is threadsafe, and whose attributes can
+ * not be modified after instantiation.
+ */
+@ToString
+@EqualsAndHashCode
+@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
+public final class ImmutableStructure implements Structure {
+
+ private final Map attributes;
+
+ /**
+ * create an immutable structure with the empty attributes.
+ */
+ public ImmutableStructure() {
+ this(new HashMap<>());
+ }
+
+ /**
+ * create immutable structure with the given attributes.
+ *
+ * @param attributes attributes.
+ */
+ public ImmutableStructure(Map attributes) {
+ Map copy = attributes.entrySet()
+ .stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().clone()));
+ this.attributes = new HashMap<>(copy);
+ }
+
+ @Override
+ public Set keySet() {
+ return new HashSet<>(this.attributes.keySet());
+ }
+
+ // getters
+ @Override
+ public Value getValue(String key) {
+ Value value = this.attributes.get(key);
+ return value.clone();
+ }
+
+ /**
+ * Get all values.
+ *
+ * @return all attributes on the structure
+ */
+ @Override
+ public Map asMap() {
+ return attributes
+ .entrySet()
+ .stream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ e -> getValue(e.getKey())
+ ));
+ }
+
+ /**
+ * Get all values, with primitives types.
+ *
+ * @return all attributes on the structure into a Map
+ */
+ @Override
+ public Map asObjectMap() {
+ return attributes
+ .entrySet()
+ .stream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ e -> convertValue(getValue(e.getKey()))
+ ));
+ }
+}
diff --git a/src/main/java/dev/openfeature/sdk/MutableContext.java b/src/main/java/dev/openfeature/sdk/MutableContext.java
index b11503c2..6abd74a5 100644
--- a/src/main/java/dev/openfeature/sdk/MutableContext.java
+++ b/src/main/java/dev/openfeature/sdk/MutableContext.java
@@ -1,7 +1,6 @@
package dev.openfeature.sdk;
import java.time.Instant;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -92,18 +91,25 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
return new MutableContext(this.asMap());
}
- Map merged = new HashMap();
+ Map merged = this.merge(map -> new MutableStructure(map),
+ this.asMap(),
+ overridingContext.asMap());
- merged.putAll(this.asMap());
- merged.putAll(overridingContext.asMap());
- EvaluationContext ec = new MutableContext(merged);
+ String newTargetingKey = "";
if (this.getTargetingKey() != null && !this.getTargetingKey().trim().equals("")) {
- ec.setTargetingKey(this.getTargetingKey());
+ newTargetingKey = this.getTargetingKey();
}
if (overridingContext.getTargetingKey() != null && !overridingContext.getTargetingKey().trim().equals("")) {
- ec.setTargetingKey(overridingContext.getTargetingKey());
+ newTargetingKey = overridingContext.getTargetingKey();
+ }
+
+ EvaluationContext ec = null;
+ if (newTargetingKey != null && !newTargetingKey.trim().equals("")) {
+ ec = new MutableContext(newTargetingKey, merged);
+ } else {
+ ec = new MutableContext(merged);
}
return ec;
diff --git a/src/main/java/dev/openfeature/sdk/MutableStructure.java b/src/main/java/dev/openfeature/sdk/MutableStructure.java
index 43a32f62..343fe2c9 100644
--- a/src/main/java/dev/openfeature/sdk/MutableStructure.java
+++ b/src/main/java/dev/openfeature/sdk/MutableStructure.java
@@ -1,5 +1,8 @@
package dev.openfeature.sdk;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
@@ -7,10 +10,6 @@
import java.util.Set;
import java.util.stream.Collectors;
-import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
/**
* {@link MutableStructure} represents a potentially nested object type which is used to represent
* structured data.
@@ -109,53 +108,4 @@ public Map asObjectMap() {
e -> convertValue(getValue(e.getKey()))
));
}
-
- /**
- * convertValue is converting the object type Value in a primitive type.
- *
- * @param value - Value object to convert
- * @return an Object containing the primitive type.
- */
- private Object convertValue(Value value) {
- if (value.isBoolean()) {
- return value.asBoolean();
- }
-
- if (value.isNumber()) {
- Double valueAsDouble = value.asDouble();
- if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
- return value.asInteger();
- }
- return valueAsDouble;
- }
-
- if (value.isString()) {
- return value.asString();
- }
-
- if (value.isInstant()) {
- return value.asInstant();
- }
-
- if (value.isList()) {
- return value.asList()
- .stream()
- .map(this::convertValue)
- .collect(Collectors.toList());
- }
-
- if (value.isStructure()) {
- Structure s = value.asStructure();
- return s.asMap()
- .keySet()
- .stream()
- .collect(
- Collectors.toMap(
- key -> key,
- key -> convertValue(s.getValue(key))
- )
- );
- }
- throw new ValueNotConvertableError();
- }
}
diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
index 74690fe9..a13eb942 100644
--- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
+++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
@@ -93,7 +93,7 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
() -> FlagEvaluationOptions.builder().build());
Map hints = Collections.unmodifiableMap(flagOptions.getHookHints());
- ctx = ObjectUtils.defaultIfNull(ctx, () -> new MutableContext());
+ ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext());
FlagEvaluationDetails details = null;
@@ -120,10 +120,10 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key
// merge of: API.context, client.context, invocation.context
apiContext = openfeatureApi.getEvaluationContext() != null
? openfeatureApi.getEvaluationContext()
- : new MutableContext();
+ : new ImmutableContext();
clientContext = this.getEvaluationContext() != null
? this.getEvaluationContext()
- : new MutableContext();
+ : new ImmutableContext();
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
@@ -137,7 +137,7 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key
details = FlagEvaluationDetails.from(providerEval, key);
hookSupport.afterHooks(type, hookCtx, details, mergedHooks, hints);
} catch (Exception e) {
- log.error("Unable to correctly evaluate flag with key {} due to exception {}", key, e.getMessage());
+ log.error("Unable to correctly evaluate flag with key '{}'", key, e);
if (details == null) {
details = FlagEvaluationDetails.builder().build();
}
diff --git a/src/main/java/dev/openfeature/sdk/Structure.java b/src/main/java/dev/openfeature/sdk/Structure.java
index 5c67551f..9d66c2be 100644
--- a/src/main/java/dev/openfeature/sdk/Structure.java
+++ b/src/main/java/dev/openfeature/sdk/Structure.java
@@ -1,7 +1,13 @@
package dev.openfeature.sdk;
+import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
+
+import java.util.HashMap;
import java.util.Map;
import java.util.Set;
+import java.util.Map.Entry;
+import java.util.function.Function;
+import java.util.stream.Collectors;
/**
* {@link Structure} represents a potentially nested object type which is used to represent
@@ -38,4 +44,82 @@ public interface Structure {
* @return all attributes on the structure into a Map
*/
Map asObjectMap();
+
+ /**
+ * convertValue is converting the object type Value in a primitive type.
+ *
+ * @param value - Value object to convert
+ * @return an Object containing the primitive type.
+ */
+ default Object convertValue(Value value) {
+ if (value.isBoolean()) {
+ return value.asBoolean();
+ }
+
+ if (value.isNumber()) {
+ Double valueAsDouble = value.asDouble();
+ if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
+ return value.asInteger();
+ }
+ return valueAsDouble;
+ }
+
+ if (value.isString()) {
+ return value.asString();
+ }
+
+ if (value.isInstant()) {
+ return value.asInstant();
+ }
+
+ if (value.isList()) {
+ return value.asList()
+ .stream()
+ .map(this::convertValue)
+ .collect(Collectors.toList());
+ }
+
+ if (value.isStructure()) {
+ Structure s = value.asStructure();
+ return s.asMap()
+ .keySet()
+ .stream()
+ .collect(
+ Collectors.toMap(
+ key -> key,
+ key -> convertValue(s.getValue(key))
+ )
+ );
+ }
+ throw new ValueNotConvertableError();
+ }
+
+ /**
+ * Recursively merges the base map with the overriding map.
+ *
+ * @param Structure type
+ * @param newStructure function to create the right structure
+ * @param base base map to merge
+ * @param overriding overriding map to merge
+ * @return resulting merged map
+ */
+ default Map merge(Function