Skip to content

Commit d9abf01

Browse files
committed
add AttributeProvider
1 parent d5cd381 commit d9abf01

File tree

6 files changed

+212
-98
lines changed

6 files changed

+212
-98
lines changed

lib/shared/common/src/main/java/com/launchdarkly/sdk/AttributeMap.java

Lines changed: 0 additions & 87 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.launchdarkly.sdk;
2+
3+
/**
4+
* An interface which can dynamically provide attribute values.
5+
*/
6+
public interface AttributeProvider {
7+
/**
8+
* Provides a value for rule evaluation.
9+
* @param key the name of the value
10+
* @return a value, or null indicating the provider is not able to provide a value
11+
*/
12+
LDValue getValue(String key);
13+
14+
/**
15+
* Provides keys for logging and event collection. These keys will be used to call {@link #getValue}.
16+
* @return all of the keys which this implementation can provide
17+
*/
18+
Iterable<String> getKeys();
19+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.launchdarkly.sdk;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
abstract class Attributes {
7+
protected final Attributes parent;
8+
9+
Attributes(Attributes parent) {
10+
this.parent = parent;
11+
}
12+
13+
abstract LDValue getInternal(String key);
14+
15+
abstract Attributes put(String key, LDValue value);
16+
17+
abstract Attributes remove(String key);
18+
19+
abstract Iterable<String> keys();
20+
21+
LDValue get(String key) {
22+
Attributes current = this;
23+
while (current != null) {
24+
LDValue value = current.getInternal(key);
25+
if (value != null) {
26+
if (value.isNull()) {
27+
break;
28+
}
29+
return value;
30+
}
31+
current = current.parent;
32+
}
33+
return null;
34+
}
35+
36+
@Override
37+
public final int hashCode() {
38+
return flatten().hashCode();
39+
}
40+
41+
@Override
42+
public final boolean equals(Object other) {
43+
if (this == other) {
44+
return true;
45+
}
46+
if (!(other instanceof Attributes)) {
47+
return false;
48+
}
49+
Attributes o = (Attributes) other;
50+
return flatten().equals(o.flatten());
51+
}
52+
53+
Map<String, LDValue> flatten() {
54+
Map<String, LDValue> out = new HashMap<>();
55+
flattenRecursive(out);
56+
return out;
57+
}
58+
59+
private final void flattenRecursive(Map<String, LDValue> out) {
60+
if (parent != null) {
61+
parent.flattenRecursive(out);
62+
}
63+
for (String key : keys()) {
64+
LDValue value = getInternal(key);
65+
if (value.isNull()) {
66+
out.remove(key);
67+
} else {
68+
out.put(key, value);
69+
}
70+
}
71+
}
72+
73+
static final class OfMap extends Attributes {
74+
private final HashMap<String, LDValue> map;
75+
76+
OfMap() {
77+
this(null);
78+
}
79+
80+
OfMap(Attributes parent) {
81+
super(parent);
82+
this.map = new HashMap<>();
83+
}
84+
85+
LDValue getInternal(String key) {
86+
return map.get(key);
87+
}
88+
89+
Attributes put(String key, LDValue value) {
90+
map.put(key, value);
91+
return this;
92+
}
93+
94+
Attributes remove(String key) {
95+
if (parent == null) {
96+
map.remove(key);
97+
} else{
98+
// we need to hide the value from the parents
99+
map.put(key, LDValue.ofNull());
100+
}
101+
return this;
102+
}
103+
104+
Iterable<String> keys() {
105+
return map.keySet();
106+
}
107+
108+
Map<String, LDValue> flatten() {
109+
// fast path, when no flattening is needed
110+
if (parent == null) {
111+
return map;
112+
}
113+
return super.flatten();
114+
}
115+
}
116+
117+
static final class OfProvider extends Attributes {
118+
private final AttributeProvider provider;
119+
120+
OfProvider(Attributes parent, AttributeProvider provider) {
121+
super(parent);
122+
this.provider = provider;
123+
}
124+
125+
LDValue getInternal(String key) {
126+
return provider.getValue(key);
127+
}
128+
129+
Attributes put(String key, LDValue value) {
130+
return new OfMap(this).put(key, value);
131+
}
132+
133+
Attributes remove(String key) {
134+
return new OfMap(this).remove(key);
135+
}
136+
137+
Iterable<String> keys() {
138+
return provider.getKeys();
139+
}
140+
}
141+
}

lib/shared/common/src/main/java/com/launchdarkly/sdk/ContextBuilder.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public final class ContextBuilder {
3232
private ContextKind kind;
3333
private String key;
3434
private String name;
35-
private AttributeMap attributes;
35+
private Attributes attributes;
3636
private boolean anonymous;
3737
private List<AttributeRef> privateAttributes;
3838
private boolean copyOnWriteAttributes;
@@ -301,23 +301,35 @@ public boolean trySet(String attributeName, LDValue value) {
301301
return false;
302302
default:
303303
if (copyOnWriteAttributes) {
304-
attributes = new AttributeMap(attributes);
304+
attributes = new Attributes.OfMap(attributes);
305305
copyOnWriteAttributes = false;
306306
}
307307
if (value == null || value.isNull()) {
308308
if (attributes != null) {
309-
attributes.remove(attributeName);
309+
attributes = attributes.remove(attributeName);
310310
}
311311
} else {
312312
if (attributes == null) {
313-
attributes = new AttributeMap();
313+
attributes = new Attributes.OfMap();
314314
}
315-
attributes.put(attributeName, value);
315+
attributes = attributes.put(attributeName, value);
316316
}
317317
}
318318
return true;
319319
}
320320

321+
/**
322+
* Dynamically (and lazily) get attribute values from a provider.
323+
* Any existing attributes previously accumulated on this builder
324+
* will be used if the provider does not provide a value.
325+
* @param attributeProvider the provider
326+
* @return the builder
327+
*/
328+
public ContextBuilder attributes(AttributeProvider attributeProvider) {
329+
attributes = new Attributes.OfProvider(attributes, attributeProvider);
330+
return this;
331+
}
332+
321333
/**
322334
* Designates any number of context attributes, or properties within them, as private:
323335
* that is, their values will not be recorded by LaunchDarkly.

lib/shared/common/src/main/java/com/launchdarkly/sdk/LDContext.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public final class LDContext implements JsonSerializable {
5858
final String key;
5959
final String fullyQualifiedKey;
6060
final String name;
61-
final AttributeMap attributes;
61+
final Attributes attributes;
6262
final boolean anonymous;
6363
final List<AttributeRef> privateAttributes;
6464

@@ -68,7 +68,7 @@ private LDContext(
6868
String key,
6969
String fullyQualifiedKey,
7070
String name,
71-
AttributeMap attributes,
71+
Attributes attributes,
7272
boolean anonymous,
7373
List<AttributeRef> privateAttributes
7474
) {
@@ -100,7 +100,7 @@ static LDContext createSingle(
100100
ContextKind kind,
101101
String key,
102102
String name,
103-
AttributeMap attributes,
103+
Attributes attributes,
104104
boolean anonymous,
105105
List<AttributeRef> privateAttributes,
106106
boolean allowEmptyKey // allowEmptyKey is true only when deserializing old-style user JSON
@@ -300,22 +300,22 @@ public static LDContext fromUser(LDUser user) {
300300
return failed(Errors.CONTEXT_NO_KEY);
301301
}
302302
}
303-
AttributeMap attributes = null;
303+
Attributes attributes = null;
304304
for (UserAttribute a: UserAttribute.OPTIONAL_STRING_ATTRIBUTES) {
305305
if (a == UserAttribute.NAME) {
306306
continue;
307307
}
308308
LDValue value = user.getAttribute(a);
309309
if (!value.isNull()) {
310310
if (attributes == null) {
311-
attributes = new AttributeMap();
311+
attributes = new Attributes.OfMap();
312312
}
313313
attributes.put(a.getName(), value);
314314
}
315315
}
316316
if (user.custom != null && !user.custom.isEmpty()) {
317317
if (attributes == null) {
318-
attributes = new AttributeMap();
318+
attributes = new Attributes.OfMap();
319319
}
320320
for (Map.Entry<UserAttribute, LDValue> kv: user.custom.entrySet()) {
321321
attributes.put(kv.getKey().getName(), kv.getValue());

lib/shared/common/src/test/java/com/launchdarkly/sdk/ContextBuilderTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.HashMap;
66
import java.util.List;
77
import java.util.Map;
8+
import java.util.Arrays;
89

910
import static com.launchdarkly.sdk.LDContextTest.kind1;
1011
import static com.launchdarkly.sdk.LDContextTest.kind2;
@@ -108,6 +109,34 @@ public void copyOnWriteAttributes2() {
108109
assertThat(c2.attributes.flatten(), equalTo(c2Map));
109110
}
110111

112+
@Test
113+
public void provider() {
114+
AttributeProvider provider = new AttributeProvider() {
115+
public LDValue getValue(String key) {
116+
switch(key) {
117+
case "a":
118+
return LDValue.of(1);
119+
case "b":
120+
return LDValue.of(2);
121+
default:
122+
return null;
123+
}
124+
}
125+
public Iterable<String> getKeys() {
126+
return Arrays.asList("a", "b");
127+
}
128+
};
129+
LDContext c1 = LDContext.builder("key").set("a", 100).attributes(provider).set("c", 3).build();
130+
assertThat(c1.getValue("a"), equalTo(LDValue.of(1)));
131+
assertThat(c1.getValue("b"), equalTo(LDValue.of(2)));
132+
assertThat(c1.getValue("c"), equalTo(LDValue.of(3)));
133+
Map<String, LDValue> c1Map = new HashMap<>();
134+
c1Map.put("a", LDValue.of(1));
135+
c1Map.put("b", LDValue.of(2));
136+
c1Map.put("c", LDValue.of(3));
137+
assertThat(c1.attributes.flatten(), equalTo(c1Map));
138+
}
139+
111140
@Test
112141
public void privateAttributes() {
113142
LDContext c1 = LDContext.create("a");

0 commit comments

Comments
 (0)