-
Notifications
You must be signed in to change notification settings - Fork 55
dr/v2 #54
dr/v2 #54
Changes from all commits
58037f8
97776e7
02b52d6
e0b6a7d
1ff3b1d
2f501c5
a7c9ae7
3c84d40
318adac
72615d4
c970f66
ede18b3
fa61ceb
3b25ab5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,20 +19,21 @@ repositories { | |
|
|
||
| allprojects { | ||
| group = 'com.launchdarkly' | ||
| version = "1.0.1" | ||
| version = "2.0.0-SNAPSHOT" | ||
| sourceCompatibility = 1.7 | ||
| targetCompatibility = 1.7 | ||
| } | ||
|
|
||
| dependencies { | ||
| compile "org.apache.httpcomponents:httpclient:4.3.6" | ||
| compile "org.apache.httpcomponents:httpclient-cache:4.3.6" | ||
| compile "commons-codec:commons-codec:1.5" | ||
| compile "com.google.code.gson:gson:2.2.4" | ||
| compile "org.apache.httpcomponents:httpclient:4.5.2" | ||
| compile "org.apache.httpcomponents:httpclient-cache:4.5.2" | ||
| compile "commons-codec:commons-codec:1.10" | ||
| compile "com.google.code.gson:gson:2.6.2" | ||
| compile "com.google.guava:guava:19.0" | ||
| compile "org.slf4j:slf4j-api:1.7.7" | ||
| compile "joda-time:joda-time:2.9.3" | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was hesitant to bring in one more dependency, but Joda is more the gold standard for date/time in Java than java.util.Date
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's any way to do what we need to do without it, I would recommend doing so. Third party dependencies have been problematic in the Java SDK in some very strange and unexpected ways. If we absolutely need this, I might suggest shading it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking we should always shade this jar anyway. Adding that to my tasks. |
||
| compile "org.slf4j:slf4j-api:1.7.21" | ||
| compile group: "com.launchdarkly", name: "okhttp-eventsource", version: "0.2.1", changing: true | ||
| compile "redis.clients:jedis:2.8.0" | ||
| compile "redis.clients:jedis:2.8.1" | ||
| testCompile "org.easymock:easymock:3.4" | ||
| testCompile 'junit:junit:4.12' | ||
| testRuntime "ch.qos.logback:logback-classic:1.1.7" | ||
|
|
@@ -56,7 +57,7 @@ buildscript { | |
| mavenLocal() | ||
| } | ||
| dependencies { | ||
| classpath 'org.ajoberstar:gradle-git:0.12.0' | ||
| classpath 'org.ajoberstar:gradle-git:1.5.0-rc.1' | ||
| classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.3' | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package com.launchdarkly.client; | ||
|
|
||
| import com.google.gson.JsonArray; | ||
| import com.google.gson.JsonElement; | ||
| import com.google.gson.JsonPrimitive; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| class Clause { | ||
| private final static Logger logger = LoggerFactory.getLogger(Clause.class); | ||
|
|
||
| private String attribute; | ||
| private Operator op; | ||
| private List<JsonPrimitive> values; //interpreted as an OR of values | ||
| private boolean negate; | ||
|
|
||
| boolean matchesUser(LDUser user) { | ||
| JsonElement userValue = valueOf(user, attribute); | ||
| if (userValue == null) { | ||
| return false; | ||
| } | ||
|
|
||
| if (userValue.isJsonArray()) { | ||
| JsonArray array = userValue.getAsJsonArray(); | ||
| for (JsonElement jsonElement : array) { | ||
| if (!jsonElement.isJsonPrimitive()) { | ||
| logger.error("Invalid custom attribute value in user object: " + jsonElement); | ||
| return false; | ||
| } | ||
| if (matchAny(jsonElement.getAsJsonPrimitive())) { | ||
| return maybeNegate(true); | ||
| } | ||
| } | ||
| return maybeNegate(false); | ||
| } else if (userValue.isJsonPrimitive()) { | ||
| return maybeNegate(matchAny(userValue.getAsJsonPrimitive())); | ||
| } | ||
| logger.warn("Got unexpected user attribute type: " + userValue.getClass().getName() + " for user key: " | ||
| + user.getKey() + " and attribute: " + attribute); | ||
| return false; | ||
| } | ||
|
|
||
| private boolean matchAny(JsonPrimitive userValue) { | ||
| for (JsonPrimitive v : values) { | ||
| if (op.apply(userValue, v)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private boolean maybeNegate(boolean b) { | ||
| if (negate) | ||
| return !b; | ||
| else | ||
| return b; | ||
| } | ||
|
|
||
| static JsonElement valueOf(LDUser user, String attribute) { | ||
| switch (attribute) { | ||
| case "key": | ||
| return user.getKey(); | ||
| case "ip": | ||
| return user.getIp(); | ||
| case "country": | ||
| return user.getCountry(); | ||
| case "email": | ||
| return user.getEmail(); | ||
| case "firstName": | ||
| return user.getFirstName(); | ||
| case "lastName": | ||
| return user.getLastName(); | ||
| case "avatar": | ||
| return user.getAvatar(); | ||
| case "name": | ||
| return user.getName(); | ||
| case "anonymous": | ||
| return user.getAnonymous(); | ||
| } | ||
| return user.getCustom(attribute); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| package com.launchdarkly.client; | ||
|
|
||
| import com.google.gson.Gson; | ||
| import com.google.gson.JsonElement; | ||
| import com.google.gson.reflect.TypeToken; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.lang.reflect.Type; | ||
| import java.util.*; | ||
|
|
||
| class FeatureFlag { | ||
| private final static Logger logger = LoggerFactory.getLogger(FeatureFlag.class); | ||
|
|
||
| private static final Gson gson = new Gson(); | ||
| private static final Type mapType = new TypeToken<Map<String, FeatureFlag>>() { | ||
| }.getType(); | ||
|
|
||
| private final String key; | ||
| private final int version; | ||
| private final boolean on; | ||
| private final List<Prerequisite> prerequisites; | ||
| private final String salt; | ||
| private final List<Target> targets; | ||
| private final List<Rule> rules; | ||
| private final Rule fallthrough; | ||
| private final Integer offVariation; //optional | ||
| private final List<JsonElement> variations; | ||
| private final boolean deleted; | ||
|
|
||
| static FeatureFlag fromJson(String json) { | ||
| return gson.fromJson(json, FeatureFlag.class); | ||
| } | ||
|
|
||
| static Map<String, FeatureFlag> fromJsonMap(String json) { | ||
| return gson.fromJson(json, mapType); | ||
| } | ||
|
|
||
| FeatureFlag(String key, int version, boolean on, List<Prerequisite> prerequisites, String salt, List<Target> targets, List<Rule> rules, Rule fallthrough, Integer offVariation, List<JsonElement> variations, boolean deleted) { | ||
| this.key = key; | ||
| this.version = version; | ||
| this.on = on; | ||
| this.prerequisites = prerequisites; | ||
| this.salt = salt; | ||
| this.targets = targets; | ||
| this.rules = rules; | ||
| this.fallthrough = fallthrough; | ||
| this.offVariation = offVariation; | ||
| this.variations = variations; | ||
| this.deleted = deleted; | ||
| } | ||
|
|
||
| Integer getOffVariation() { | ||
| return this.offVariation; | ||
| } | ||
|
|
||
| JsonElement getOffVariationValue() { | ||
| if (offVariation != null && offVariation < variations.size()) { | ||
| return variations.get(offVariation); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| EvalResult evaluate(LDUser user, FeatureStore featureStore) { | ||
| if (user == null || user.getKey() == null) { | ||
| return null; | ||
| } | ||
| List<FeatureRequestEvent> prereqEvents = new ArrayList<>(); | ||
| Set<String> visited = new HashSet<>(); | ||
| return evaluate(user, featureStore, prereqEvents, visited); | ||
| } | ||
|
|
||
| private EvalResult evaluate(LDUser user, FeatureStore featureStore, List<FeatureRequestEvent> events, Set<String> visited) { | ||
| for (Prerequisite prereq : prerequisites) { | ||
| visited.add(key); | ||
| if (visited.contains(prereq.getKey())) { | ||
| logger.error("Prerequisite cycle detected when evaluating feature flag: " + key); | ||
| return null; | ||
| } | ||
| FeatureFlag prereqFeatureFlag = featureStore.get(prereq.getKey()); | ||
| if (prereqFeatureFlag == null) { | ||
| logger.error("Could not retrieve prerequisite flag: " + prereq.getKey() + " when evaluating: " + key); | ||
| return null; | ||
| } | ||
| JsonElement prereqValue; | ||
| if (prereqFeatureFlag.isOn()) { | ||
| EvalResult prereqEvalResult = prereqFeatureFlag.evaluate(user, featureStore, events, visited); | ||
| if (prereqEvalResult == null) { | ||
| return null; | ||
| } | ||
| prereqValue = prereqEvalResult.value; | ||
| visited = prereqEvalResult.visitedFeatureKeys; | ||
| events = prereqEvalResult.prerequisiteEvents; | ||
| events.add(new FeatureRequestEvent(prereqFeatureFlag.getKey(), user, prereqValue, null)); | ||
| if (prereqValue == null || !prereqValue.equals(prereqFeatureFlag.getVariation(prereq.getVariation()))) { | ||
| return new EvalResult(null, events, visited); | ||
| } | ||
| } else { | ||
| return null; | ||
| } | ||
| } | ||
| return new EvalResult(getVariation(evaluateIndex(user)), events, visited); | ||
| } | ||
|
|
||
| private Integer evaluateIndex(LDUser user) { | ||
| // Check to see if targets match | ||
| for (Target target : targets) { | ||
| for (String v : target.getValues()) { | ||
| if (v.equals(user.getKey().getAsString())) { | ||
| return target.getVariation(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Now walk through the rules and see if any match | ||
| for (Rule rule : rules) { | ||
| if (rule.matchesUser(user)) { | ||
| return rule.variationIndexForUser(user, key, salt); | ||
| } | ||
| } | ||
|
|
||
| // Walk through the fallthrough and see if it matches | ||
| return fallthrough.variationIndexForUser(user, key, salt); | ||
| } | ||
|
|
||
| private JsonElement getVariation(Integer index) { | ||
| if (index == null || index >= variations.size()) { | ||
| return null; | ||
| } else { | ||
| return variations.get(index); | ||
| } | ||
| } | ||
|
|
||
| int getVersion() { | ||
| return version; | ||
| } | ||
|
|
||
| String getKey() { | ||
| return key; | ||
| } | ||
|
|
||
| boolean isDeleted() { | ||
| return deleted; | ||
| } | ||
|
|
||
| boolean isOn() { | ||
| return on; | ||
| } | ||
|
|
||
| List<Prerequisite> getPrerequisites() { | ||
| return prerequisites; | ||
| } | ||
|
|
||
| String getSalt() { | ||
| return salt; | ||
| } | ||
|
|
||
| List<Target> getTargets() { | ||
| return targets; | ||
| } | ||
|
|
||
| List<Rule> getRules() { | ||
| return rules; | ||
| } | ||
|
|
||
| Rule getFallthrough() { | ||
| return fallthrough; | ||
| } | ||
|
|
||
| List<JsonElement> getVariations() { | ||
| return variations; | ||
| } | ||
|
|
||
| static class EvalResult { | ||
| private JsonElement value; | ||
| private List<FeatureRequestEvent> prerequisiteEvents; | ||
| private Set<String> visitedFeatureKeys; | ||
|
|
||
| private EvalResult(JsonElement value, List<FeatureRequestEvent> prerequisiteEvents, Set<String> visitedFeatureKeys) { | ||
| this.value = value; | ||
| this.prerequisiteEvents = prerequisiteEvents; | ||
| this.visitedFeatureKeys = visitedFeatureKeys; | ||
| } | ||
|
|
||
| JsonElement getValue() { | ||
| return value; | ||
| } | ||
|
|
||
| List<FeatureRequestEvent> getPrerequisiteEvents() { | ||
| return prerequisiteEvents; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated all the deps to latest releases.