diff --git a/lib/shared/common/src/main/java/com/launchdarkly/sdk/AttributeMap.java b/lib/shared/common/src/main/java/com/launchdarkly/sdk/AttributeMap.java deleted file mode 100644 index 9ca1c2d..0000000 --- a/lib/shared/common/src/main/java/com/launchdarkly/sdk/AttributeMap.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.launchdarkly.sdk; - -import java.util.HashMap; -import java.util.Map; - -final class AttributeMap { - private final AttributeMap parent; - private final Map map; - - AttributeMap() { - this(null); - } - - AttributeMap(AttributeMap parent) { - this.parent = parent; - this.map = new HashMap<>(); - } - - LDValue get(String key) { - AttributeMap current = this; - while (current != null) { - LDValue value = current.map.get(key); - if (value != null) { - if (value.isNull()) { - break; - } - return value; - } - current = current.parent; - } - return null; - } - - void put(String key, LDValue value) { - map.put(key, value); - } - - @Override - public int hashCode() { - return flatten().hashCode(); - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof AttributeMap)) { - return false; - } - AttributeMap o = (AttributeMap) other; - return flatten().equals(o.flatten()); - } - - Map flatten() { - if (parent == null) { - return map; - } - Map out = new HashMap<>(); - flattenRecursive(out); - return out; - } - - private void flattenRecursive(Map out) { - if (parent != null) { - parent.flattenRecursive(out); - } - for (Map.Entry entry : map.entrySet()) { - String key = entry.getKey(); - LDValue value = entry.getValue(); - if (value.isNull()) { - out.remove(key); - } else { - out.put(key, value); - } - } - } - - void remove(String key) { - if (parent == null) { - map.remove(key); - return; - } - // we need to hide the value from the parents - map.put(key, LDValue.ofNull()); - } -} \ No newline at end of file diff --git a/lib/shared/common/src/main/java/com/launchdarkly/sdk/AttributeProvider.java b/lib/shared/common/src/main/java/com/launchdarkly/sdk/AttributeProvider.java new file mode 100644 index 0000000..7dd9827 --- /dev/null +++ b/lib/shared/common/src/main/java/com/launchdarkly/sdk/AttributeProvider.java @@ -0,0 +1,19 @@ +package com.launchdarkly.sdk; + +/** + * An interface which can dynamically provide attribute values. + */ +public interface AttributeProvider { + /** + * Provides a value for rule evaluation. + * @param key the name of the value + * @return a value, or null indicating the provider is not able to provide a value + */ + LDValue getValue(String key); + + /** + * Provides keys for logging and event collection. These keys will be used to call {@link #getValue}. + * @return all of the keys which this implementation can provide + */ + Iterable getKeys(); +} \ No newline at end of file diff --git a/lib/shared/common/src/main/java/com/launchdarkly/sdk/Attributes.java b/lib/shared/common/src/main/java/com/launchdarkly/sdk/Attributes.java new file mode 100644 index 0000000..c7fe04c --- /dev/null +++ b/lib/shared/common/src/main/java/com/launchdarkly/sdk/Attributes.java @@ -0,0 +1,141 @@ +package com.launchdarkly.sdk; + +import java.util.HashMap; +import java.util.Map; + +abstract class Attributes { + protected final Attributes parent; + + Attributes(Attributes parent) { + this.parent = parent; + } + + abstract LDValue getInternal(String key); + + abstract Attributes put(String key, LDValue value); + + abstract Attributes remove(String key); + + abstract Iterable keys(); + + LDValue get(String key) { + Attributes current = this; + while (current != null) { + LDValue value = current.getInternal(key); + if (value != null) { + if (value.isNull()) { + break; + } + return value; + } + current = current.parent; + } + return null; + } + + @Override + public final int hashCode() { + return flatten().hashCode(); + } + + @Override + public final boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Attributes)) { + return false; + } + Attributes o = (Attributes) other; + return flatten().equals(o.flatten()); + } + + Map flatten() { + Map out = new HashMap<>(); + flattenRecursive(out); + return out; + } + + private final void flattenRecursive(Map out) { + if (parent != null) { + parent.flattenRecursive(out); + } + for (String key : keys()) { + LDValue value = getInternal(key); + if (value.isNull()) { + out.remove(key); + } else { + out.put(key, value); + } + } + } + + static final class OfMap extends Attributes { + private final HashMap map; + + OfMap() { + this(null); + } + + OfMap(Attributes parent) { + super(parent); + this.map = new HashMap<>(); + } + + LDValue getInternal(String key) { + return map.get(key); + } + + Attributes put(String key, LDValue value) { + map.put(key, value); + return this; + } + + Attributes remove(String key) { + if (parent == null) { + map.remove(key); + } else{ + // we need to hide the value from the parents + map.put(key, LDValue.ofNull()); + } + return this; + } + + Iterable keys() { + return map.keySet(); + } + + Map flatten() { + // fast path, when no flattening is needed + if (parent == null) { + return map; + } + return super.flatten(); + } + } + + static final class OfProvider extends Attributes { + private final AttributeProvider provider; + + OfProvider(Attributes parent, AttributeProvider provider) { + super(parent); + this.provider = provider; + } + + LDValue getInternal(String key) { + return provider.getValue(key); + } + + Attributes put(String key, LDValue value) { + return new OfMap(this).put(key, value); + } + + Attributes remove(String key) { + return new OfMap(this).remove(key); + } + + Iterable keys() { + return provider.getKeys(); + } + } +} \ No newline at end of file diff --git a/lib/shared/common/src/main/java/com/launchdarkly/sdk/ContextBuilder.java b/lib/shared/common/src/main/java/com/launchdarkly/sdk/ContextBuilder.java index d9e5fa5..157fd66 100644 --- a/lib/shared/common/src/main/java/com/launchdarkly/sdk/ContextBuilder.java +++ b/lib/shared/common/src/main/java/com/launchdarkly/sdk/ContextBuilder.java @@ -32,7 +32,7 @@ public final class ContextBuilder { private ContextKind kind; private String key; private String name; - private AttributeMap attributes; + private Attributes attributes; private boolean anonymous; private List privateAttributes; private boolean copyOnWriteAttributes; @@ -301,23 +301,35 @@ public boolean trySet(String attributeName, LDValue value) { return false; default: if (copyOnWriteAttributes) { - attributes = new AttributeMap(attributes); + attributes = new Attributes.OfMap(attributes); copyOnWriteAttributes = false; } if (value == null || value.isNull()) { if (attributes != null) { - attributes.remove(attributeName); + attributes = attributes.remove(attributeName); } } else { if (attributes == null) { - attributes = new AttributeMap(); + attributes = new Attributes.OfMap(); } - attributes.put(attributeName, value); + attributes = attributes.put(attributeName, value); } } return true; } + /** + * Dynamically (and lazily) get attribute values from a provider. + * Any existing attributes previously accumulated on this builder + * will be used if the provider does not provide a value. + * @param attributeProvider the provider + * @return the builder + */ + public ContextBuilder attributes(AttributeProvider attributeProvider) { + attributes = new Attributes.OfProvider(attributes, attributeProvider); + return this; + } + /** * Designates any number of context attributes, or properties within them, as private: * that is, their values will not be recorded by LaunchDarkly. diff --git a/lib/shared/common/src/main/java/com/launchdarkly/sdk/LDContext.java b/lib/shared/common/src/main/java/com/launchdarkly/sdk/LDContext.java index 986952f..111f97d 100644 --- a/lib/shared/common/src/main/java/com/launchdarkly/sdk/LDContext.java +++ b/lib/shared/common/src/main/java/com/launchdarkly/sdk/LDContext.java @@ -58,7 +58,7 @@ public final class LDContext implements JsonSerializable { final String key; final String fullyQualifiedKey; final String name; - final AttributeMap attributes; + final Attributes attributes; final boolean anonymous; final List privateAttributes; @@ -68,7 +68,7 @@ private LDContext( String key, String fullyQualifiedKey, String name, - AttributeMap attributes, + Attributes attributes, boolean anonymous, List privateAttributes ) { @@ -100,7 +100,7 @@ static LDContext createSingle( ContextKind kind, String key, String name, - AttributeMap attributes, + Attributes attributes, boolean anonymous, List privateAttributes, boolean allowEmptyKey // allowEmptyKey is true only when deserializing old-style user JSON @@ -300,7 +300,7 @@ public static LDContext fromUser(LDUser user) { return failed(Errors.CONTEXT_NO_KEY); } } - AttributeMap attributes = null; + Attributes attributes = null; for (UserAttribute a: UserAttribute.OPTIONAL_STRING_ATTRIBUTES) { if (a == UserAttribute.NAME) { continue; @@ -308,14 +308,14 @@ public static LDContext fromUser(LDUser user) { LDValue value = user.getAttribute(a); if (!value.isNull()) { if (attributes == null) { - attributes = new AttributeMap(); + attributes = new Attributes.OfMap(); } attributes.put(a.getName(), value); } } if (user.custom != null && !user.custom.isEmpty()) { if (attributes == null) { - attributes = new AttributeMap(); + attributes = new Attributes.OfMap(); } for (Map.Entry kv: user.custom.entrySet()) { attributes.put(kv.getKey().getName(), kv.getValue()); diff --git a/lib/shared/common/src/test/java/com/launchdarkly/sdk/ContextBuilderTest.java b/lib/shared/common/src/test/java/com/launchdarkly/sdk/ContextBuilderTest.java index 65b84ba..d02db5f 100644 --- a/lib/shared/common/src/test/java/com/launchdarkly/sdk/ContextBuilderTest.java +++ b/lib/shared/common/src/test/java/com/launchdarkly/sdk/ContextBuilderTest.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Arrays; import static com.launchdarkly.sdk.LDContextTest.kind1; import static com.launchdarkly.sdk.LDContextTest.kind2; @@ -108,6 +109,34 @@ public void copyOnWriteAttributes2() { assertThat(c2.attributes.flatten(), equalTo(c2Map)); } + @Test + public void provider() { + AttributeProvider provider = new AttributeProvider() { + public LDValue getValue(String key) { + switch(key) { + case "a": + return LDValue.of(1); + case "b": + return LDValue.of(2); + default: + return null; + } + } + public Iterable getKeys() { + return Arrays.asList("a", "b"); + } + }; + LDContext c1 = LDContext.builder("key").set("a", 100).attributes(provider).set("c", 3).build(); + assertThat(c1.getValue("a"), equalTo(LDValue.of(1))); + assertThat(c1.getValue("b"), equalTo(LDValue.of(2))); + assertThat(c1.getValue("c"), equalTo(LDValue.of(3))); + Map c1Map = new HashMap<>(); + c1Map.put("a", LDValue.of(1)); + c1Map.put("b", LDValue.of(2)); + c1Map.put("c", LDValue.of(3)); + assertThat(c1.attributes.flatten(), equalTo(c1Map)); + } + @Test public void privateAttributes() { LDContext c1 = LDContext.create("a");