Skip to content

Commit ae69411

Browse files
chrfwowVishalup29
andauthored
feat: equivalent EvaluationContext impls are .equal (#1771)
fix: .equals false for equivalent contexts (#1768)" This reverts commit 4cb39a4. Signed-off-by: Todd Baert <todd.baert@dynatrace.com> Co-authored-by: Vishalup29 <67001661+Vishalup29@users.noreply.github.com>
1 parent 2a37f46 commit ae69411

File tree

8 files changed

+446
-50
lines changed

8 files changed

+446
-50
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import java.util.Collections;
44
import java.util.HashMap;
55
import java.util.Map;
6-
import lombok.EqualsAndHashCode;
6+
import java.util.Objects;
77

88
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
9-
@EqualsAndHashCode
109
abstract class AbstractStructure implements Structure {
1110

1211
protected final Map<String, Value> attributes;
@@ -48,4 +47,18 @@ public Map<String, Object> asObjectMap() {
4847
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
4948
HashMap::putAll);
5049
}
50+
51+
@Override
52+
public boolean equals(Object object) {
53+
if (!(object instanceof AbstractStructure)) {
54+
return false;
55+
}
56+
AbstractStructure that = (AbstractStructure) object;
57+
return Objects.equals(attributes, that.attributes);
58+
}
59+
60+
@Override
61+
public int hashCode() {
62+
return Objects.hashCode(attributes);
63+
}
5164
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ public interface EvaluationContext extends Structure {
2424
*/
2525
EvaluationContext merge(EvaluationContext overridingContext);
2626

27+
/**
28+
* If the other object is an EvaluationContext, this method compares the results of asUnmodifiableMap() for
29+
* equality. Otherwise, it returns false.
30+
* <br>
31+
* <br>
32+
* Implementations of EvaluationContext are encouraged to delegate their equals() method to this method, or provide
33+
* a more optimized check with the same semantics.
34+
*
35+
* @param other the object to compare to
36+
* @return true if the other object is an EvaluationContext and has the same map representation, false otherwise
37+
*/
38+
default boolean isEqualTo(Object other) {
39+
if (other == null) {
40+
return false;
41+
}
42+
if (other == this) {
43+
return true;
44+
}
45+
if (!(other instanceof EvaluationContext)) {
46+
return false;
47+
}
48+
var otherContext = (EvaluationContext) other;
49+
return asUnmodifiableMap().equals(otherContext.asUnmodifiableMap());
50+
}
51+
2752
/**
2853
* Recursively merges the overriding map into the base Value map.
2954
* The base map is mutated, the overriding map is not.

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

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

@@ -17,7 +16,6 @@
1716
* not be modified after instantiation.
1817
*/
1918
@ToString
20-
@EqualsAndHashCode
2119
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
2220
public final class ImmutableContext implements EvaluationContext {
2321

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

27+
// Lazily computed hash code, safe because this class is immutable.
28+
private volatile Integer cachedHashCode;
29+
2930
/**
3031
* Create an immutable context with an empty targeting_key and attributes
3132
* provided.
@@ -96,6 +97,40 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
9697
return new ImmutableContext(attributes);
9798
}
9899

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+
return isEqualTo(o);
111+
}
112+
113+
/**
114+
* Computes a hash code consistent with {@link #equals(Object)}. Since this context is immutable,
115+
* the hash code is lazily computed once from its resolved attribute map and then cached.
116+
*
117+
* @return the cached hash code derived from this context's attribute map
118+
*/
119+
@Override
120+
public int hashCode() {
121+
Integer result = cachedHashCode;
122+
if (result == null) {
123+
synchronized (this) {
124+
result = cachedHashCode;
125+
if (result == null) {
126+
result = structure.hashCode();
127+
cachedHashCode = result;
128+
}
129+
}
130+
}
131+
return result;
132+
}
133+
99134
@SuppressWarnings("all")
100135
private static class DelegateExclusions {
101136
@ExcludeFromGeneratedCoverageReport

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

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ 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;
2427

2528
/**
2629
* Constructor for LayeredEvaluationContext.
@@ -174,15 +177,20 @@ public Value getValue(String key) {
174177
return getFromContext(apiContext, key);
175178
}
176179

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

183190
HashMap<String, Value> map;
184191
if (keySet != null) {
185-
map = new HashMap<>(keySet.size());
192+
// use helper to size the map based on expected entries
193+
map = HashMapUtils.forEntries(keySet.size());
186194
} else {
187195
map = new HashMap<>();
188196
}
@@ -205,7 +213,15 @@ public Map<String, Value> asMap() {
205213
map.putAll(hookContext.asMap());
206214
}
207215
}
208-
return map;
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());
209225
}
210226

211227
@Override
@@ -214,7 +230,7 @@ public Map<String, Value> asUnmodifiableMap() {
214230
return Collections.emptyMap();
215231
}
216232

217-
return Collections.unmodifiableMap(asMap());
233+
return getResolvedMap();
218234
}
219235

220236
@Override
@@ -225,7 +241,8 @@ public Map<String, Object> asObjectMap() {
225241

226242
HashMap<String, Object> map;
227243
if (keySet != null) {
228-
map = new HashMap<>(keySet.size());
244+
// use helper to size the map based on expected entries
245+
map = HashMapUtils.forEntries(keySet.size());
229246
} else {
230247
map = new HashMap<>();
231248
}
@@ -248,9 +265,33 @@ public Map<String, Object> asObjectMap() {
248265
map.putAll(hookContext.asObjectMap());
249266
}
250267
}
268+
251269
return map;
252270
}
253271

272+
@Override
273+
public boolean equals(Object o) {
274+
if (this == o) {
275+
return true;
276+
}
277+
if (!(o instanceof EvaluationContext)) {
278+
return false;
279+
}
280+
281+
EvaluationContext that = (EvaluationContext) o;
282+
283+
if (that instanceof LayeredEvaluationContext) {
284+
return this.getResolvedMap().equals(((LayeredEvaluationContext) that).getResolvedMap());
285+
}
286+
287+
return this.getResolvedMap().equals(that.asUnmodifiableMap());
288+
}
289+
290+
@Override
291+
public int hashCode() {
292+
return getResolvedMap().hashCode();
293+
}
294+
254295
void putHookContext(EvaluationContext context) {
255296
if (context == null || context.isEmpty()) {
256297
return;
@@ -265,5 +306,6 @@ void putHookContext(EvaluationContext context) {
265306
}
266307
this.hookContexts.add(context);
267308
this.keySet = null;
309+
this.cachedMap = null;
268310
}
269311
}

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import java.util.List;
77
import java.util.Map;
88
import java.util.function.Function;
9-
import lombok.EqualsAndHashCode;
109
import lombok.ToString;
1110
import lombok.experimental.Delegate;
1211

@@ -17,7 +16,6 @@
1716
* be modified after instantiation.
1817
*/
1918
@ToString
20-
@EqualsAndHashCode
2119
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
2220
public class MutableContext implements EvaluationContext {
2321

@@ -125,6 +123,24 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
125123
return new MutableContext(attributes);
126124
}
127125

126+
/**
127+
* Equality for EvaluationContext implementations is defined in terms of their resolved
128+
* attribute maps. Two contexts are considered equal if their {@link #asMap()} representations
129+
* contain the same key/value pairs, regardless of how the context was constructed or layered.
130+
*
131+
* @param o the object to compare with this context
132+
* @return true if the other object is an EvaluationContext whose resolved attributes match
133+
*/
134+
@Override
135+
public boolean equals(Object o) {
136+
return isEqualTo(o);
137+
}
138+
139+
@Override
140+
public int hashCode() {
141+
return structure.hashCode();
142+
}
143+
128144
/**
129145
* Hidden class to tell Lombok not to copy these methods over via delegation.
130146
*/

0 commit comments

Comments
 (0)