Skip to content

Commit 4cb39a4

Browse files
authored
fix: context serialization missing props (with revert) (#1768)
fix: context serialization with revert This reverts commit 6f3a30a. Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
1 parent 04feac8 commit 4cb39a4

File tree

4 files changed

+43
-159
lines changed

4 files changed

+43
-159
lines changed

src/main/java/dev/openfeature/sdk/ImmutableContext.java

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.HashMap;
66
import java.util.Map;
77
import java.util.function.Function;
8+
import lombok.EqualsAndHashCode;
89
import lombok.ToString;
910
import lombok.experimental.Delegate;
1011

@@ -16,6 +17,7 @@
1617
* not be modified after instantiation.
1718
*/
1819
@ToString
20+
@EqualsAndHashCode
1921
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
2022
public final class ImmutableContext implements EvaluationContext {
2123

@@ -24,9 +26,6 @@ public final class ImmutableContext implements EvaluationContext {
2426
@Delegate(excludes = DelegateExclusions.class)
2527
private final ImmutableStructure structure;
2628

27-
// Lazily computed hash code, safe because this class is immutable.
28-
private volatile Integer cachedHashCode;
29-
3029
/**
3130
* Create an immutable context with an empty targeting_key and attributes
3231
* provided.
@@ -97,47 +96,6 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
9796
return new ImmutableContext(attributes);
9897
}
9998

100-
/**
101-
* Equality for EvaluationContext implementations is defined in terms of their resolved
102-
* attribute maps. Two contexts are considered equal if their {@link #asMap()} representations
103-
* contain the same key/value pairs, regardless of how the context was constructed or layered.
104-
*
105-
* @param o the object to compare with this context
106-
* @return true if the other object is an EvaluationContext whose resolved attributes match
107-
*/
108-
@Override
109-
public boolean equals(Object o) {
110-
if (this == o) {
111-
return true;
112-
}
113-
if (!(o instanceof EvaluationContext)) {
114-
return false;
115-
}
116-
EvaluationContext that = (EvaluationContext) o;
117-
return this.asUnmodifiableMap().equals(that.asUnmodifiableMap());
118-
}
119-
120-
/**
121-
* Computes a hash code consistent with {@link #equals(Object)}. Since this context is immutable,
122-
* the hash code is lazily computed once from its resolved attribute map and then cached.
123-
*
124-
* @return the cached hash code derived from this context's attribute map
125-
*/
126-
@Override
127-
public int hashCode() {
128-
Integer result = cachedHashCode;
129-
if (result == null) {
130-
synchronized (this) {
131-
result = cachedHashCode;
132-
if (result == null) {
133-
result = asUnmodifiableMap().hashCode();
134-
cachedHashCode = result;
135-
}
136-
}
137-
}
138-
return result;
139-
}
140-
14199
@SuppressWarnings("all")
142100
private static class DelegateExclusions {
143101
@ExcludeFromGeneratedCoverageReport

src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java

Lines changed: 28 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ public class LayeredEvaluationContext implements EvaluationContext {
2121
private ArrayList<EvaluationContext> hookContexts;
2222
private String targetingKey;
2323
private Set<String> keySet = null;
24-
// Lazily computed resolved attribute map for this layered context.
25-
// This must be invalidated whenever the underlying layers change.
26-
private Map<String, Value> cachedMap;
2724

2825
/**
2926
* Constructor for LayeredEvaluationContext.
@@ -177,20 +174,15 @@ public Value getValue(String key) {
177174
return getFromContext(apiContext, key);
178175
}
179176

180-
private Map<String, Value> getResolvedMap() {
181-
if (cachedMap != null) {
182-
return cachedMap;
183-
}
184-
177+
@Override
178+
public Map<String, Value> asMap() {
185179
if (keySet != null && keySet.isEmpty()) {
186-
cachedMap = Collections.emptyMap();
187-
return cachedMap;
180+
return new HashMap<>(0);
188181
}
189182

190183
HashMap<String, Value> map;
191184
if (keySet != null) {
192-
// use helper to size the map based on expected entries
193-
map = HashMapUtils.forEntries(keySet.size());
185+
map = new HashMap<>(keySet.size());
194186
} else {
195187
map = new HashMap<>();
196188
}
@@ -213,15 +205,7 @@ private Map<String, Value> getResolvedMap() {
213205
map.putAll(hookContext.asMap());
214206
}
215207
}
216-
217-
cachedMap = Collections.unmodifiableMap(map);
218-
return cachedMap;
219-
}
220-
221-
@Override
222-
public Map<String, Value> asMap() {
223-
// Return a defensive copy so callers can't mutate our cached map.
224-
return new HashMap<>(getResolvedMap());
208+
return map;
225209
}
226210

227211
@Override
@@ -230,48 +214,41 @@ public Map<String, Value> asUnmodifiableMap() {
230214
return Collections.emptyMap();
231215
}
232216

233-
return getResolvedMap();
217+
return Collections.unmodifiableMap(asMap());
234218
}
235219

236220
@Override
237221
public Map<String, Object> asObjectMap() {
238-
// Build the object map directly from the resolved attribute map,
239-
// so this stays consistent with equals/hashCode and asMap().
240-
Map<String, Value> resolved = getResolvedMap();
241-
if (resolved.isEmpty()) {
222+
if (keySet != null && keySet.isEmpty()) {
242223
return new HashMap<>(0);
243224
}
244225

245-
HashMap<String, Object> map = HashMapUtils.forEntries(resolved.size());
246-
for (Map.Entry<String, Value> entry : resolved.entrySet()) {
247-
Value value = entry.getValue();
248-
// Value is responsible for exposing the underlying Java representation.
249-
map.put(entry.getKey(), value == null ? null : value.asObject());
226+
HashMap<String, Object> map;
227+
if (keySet != null) {
228+
map = new HashMap<>(keySet.size());
229+
} else {
230+
map = new HashMap<>();
250231
}
251-
return map;
252-
}
253232

254-
@Override
255-
public boolean equals(Object o) {
256-
if (this == o) {
257-
return true;
233+
if (apiContext != null) {
234+
map.putAll(apiContext.asObjectMap());
258235
}
259-
if (!(o instanceof EvaluationContext)) {
260-
return false;
236+
if (transactionContext != null) {
237+
map.putAll(transactionContext.asObjectMap());
261238
}
262-
263-
EvaluationContext that = (EvaluationContext) o;
264-
265-
if (that instanceof LayeredEvaluationContext) {
266-
return this.getResolvedMap().equals(((LayeredEvaluationContext) that).getResolvedMap());
239+
if (clientContext != null) {
240+
map.putAll(clientContext.asObjectMap());
267241
}
268-
269-
return this.getResolvedMap().equals(that.asUnmodifiableMap());
270-
}
271-
272-
@Override
273-
public int hashCode() {
274-
return getResolvedMap().hashCode();
242+
if (invocationContext != null) {
243+
map.putAll(invocationContext.asObjectMap());
244+
}
245+
if (hookContexts != null) {
246+
for (int i = 0; i < hookContexts.size(); i++) {
247+
EvaluationContext hookContext = hookContexts.get(i);
248+
map.putAll(hookContext.asObjectMap());
249+
}
250+
}
251+
return map;
275252
}
276253

277254
void putHookContext(EvaluationContext context) {
@@ -288,6 +265,5 @@ void putHookContext(EvaluationContext context) {
288265
}
289266
this.hookContexts.add(context);
290267
this.keySet = null;
291-
this.cachedMap = null;
292268
}
293269
}

src/test/java/dev/openfeature/sdk/ImmutableContextTest.java

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
55
import static org.junit.jupiter.api.Assertions.assertEquals;
66
import static org.junit.jupiter.api.Assertions.assertNotEquals;
7-
import static org.junit.jupiter.api.Assertions.assertNull;
87
import static org.junit.jupiter.api.Assertions.assertTrue;
98

10-
import java.util.Arrays;
119
import java.util.Collections;
1210
import java.util.HashMap;
13-
import java.util.HashSet;
1411
import java.util.Map;
1512
import org.junit.jupiter.api.DisplayName;
1613
import org.junit.jupiter.api.Test;
@@ -53,7 +50,7 @@ void shouldChangeTargetingKeyFromOverridingContext() {
5350
assertEquals("overriding_key", merge.getTargetingKey());
5451
}
5552

56-
@DisplayName("targeting key should not be changed from the overriding context if missing")
53+
@DisplayName("targeting key should not changed from the overriding context if missing")
5754
@Test
5855
void shouldRetainTargetingKeyWhenOverridingContextTargetingKeyValueIsEmpty() {
5956
HashMap<String, Value> attributes = new HashMap<>();
@@ -69,7 +66,7 @@ void shouldRetainTargetingKeyWhenOverridingContextTargetingKeyValueIsEmpty() {
6966
@Test
7067
void missingTargetingKeyShould() {
7168
EvaluationContext ctx = new ImmutableContext();
72-
assertNull(ctx.getTargetingKey());
69+
assertEquals(null, ctx.getTargetingKey());
7370
}
7471

7572
@DisplayName("Merge should retain all the attributes from the existing context when overriding context is null")
@@ -148,26 +145,10 @@ void mergeShouldObtainKeysFromOverridingContextWhenExistingContextIsEmpty() {
148145
EvaluationContext ctx = new ImmutableContext();
149146
EvaluationContext overriding = new ImmutableContext(attributes);
150147
EvaluationContext merge = ctx.merge(overriding);
151-
assertEquals(new HashSet<>(Arrays.asList("key1", "key2")), merge.keySet());
148+
assertEquals(new java.util.HashSet<>(java.util.Arrays.asList("key1", "key2")), merge.keySet());
152149
}
153150

154-
@DisplayName("Two ImmutableContext objects with identical attributes are considered equal")
155-
@Test
156-
void testImmutableContextEquality() {
157-
Map<String, Value> map1 = new HashMap<>();
158-
map1.put("key", new Value("value"));
159-
160-
Map<String, Value> map2 = new HashMap<>();
161-
map2.put("key", new Value("value"));
162-
163-
ImmutableContext a = new ImmutableContext(null, map1);
164-
ImmutableContext b = new ImmutableContext(null, map2);
165-
166-
assertEquals(a, b);
167-
assertEquals(a.hashCode(), b.hashCode());
168-
}
169-
170-
@DisplayName("Two different ImmutableContext objects with different contents are not considered equal")
151+
@DisplayName("Two different MutableContext objects with the different contents are not considered equal")
171152
@Test
172153
void unequalImmutableContextsAreNotEqual() {
173154
final Map<String, Value> attributes = new HashMap<>();
@@ -180,16 +161,17 @@ void unequalImmutableContextsAreNotEqual() {
180161
assertNotEquals(ctx, ctx2);
181162
}
182163

183-
@DisplayName("ImmutableContext hashCode is stable across multiple invocations")
164+
@DisplayName("Two different MutableContext objects with the same content are considered equal")
184165
@Test
185-
void immutableContextHashCodeIsStable() {
186-
Map<String, Value> map = new HashMap<>();
187-
map.put("key", new Value("value"));
166+
void equalImmutableContextsAreEqual() {
167+
final Map<String, Value> attributes = new HashMap<>();
168+
attributes.put("key1", new Value("val1"));
169+
final ImmutableContext ctx = new ImmutableContext(attributes);
188170

189-
ImmutableContext ctx = new ImmutableContext(null, map);
171+
final Map<String, Value> attributes2 = new HashMap<>();
172+
attributes2.put("key1", new Value("val1"));
173+
final ImmutableContext ctx2 = new ImmutableContext(attributes2);
190174

191-
int first = ctx.hashCode();
192-
int second = ctx.hashCode();
193-
assertEquals(first, second);
175+
assertEquals(ctx, ctx2);
194176
}
195177
}

src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -397,37 +397,5 @@ void mergesCorrectlyWhenOtherHasNoTargetingKey() {
397397
merged.asMap());
398398
assertEquals(invocationContext.getTargetingKey(), merged.getTargetingKey());
399399
}
400-
401-
@Test
402-
void testLayeredContextEquality() {
403-
Map<String, Value> baseMap = Map.of("k", new Value("v"));
404-
Map<String, Value> layerMap = Map.of("x", new Value("y"));
405-
406-
EvaluationContext base = new MutableContext(null, baseMap);
407-
EvaluationContext layer = new MutableContext(null, layerMap);
408-
409-
LayeredEvaluationContext l1 = new LayeredEvaluationContext(base, layer, null, null);
410-
LayeredEvaluationContext l2 = new LayeredEvaluationContext(base, layer, null, null);
411-
412-
assertEquals(l1, l2);
413-
assertEquals(l1.hashCode(), l2.hashCode());
414-
}
415-
416-
@Test
417-
void testMixedContextEquality() {
418-
Map<String, Value> map = Map.of("foo", new Value("bar"));
419-
420-
EvaluationContext base = new MutableContext(null, map);
421-
LayeredEvaluationContext layered = new LayeredEvaluationContext(null, null, null, base);
422-
423-
// Equality from the layered context's perspective (map-based equality)
424-
assertEquals(layered, base);
425-
426-
// Resolved maps should be identical
427-
assertEquals(base.asMap(), layered.asMap());
428-
429-
// Layered's hashCode must be consistent with its resolved attribute map
430-
assertEquals(base.asMap().hashCode(), layered.hashCode());
431-
}
432400
}
433401
}

0 commit comments

Comments
 (0)