Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.
Merged
17 changes: 9 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor Author

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.

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"
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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"
Expand All @@ -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'
}
}
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/com/launchdarkly/client/Clause.java
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);
}
}
193 changes: 193 additions & 0 deletions src/main/java/com/launchdarkly/client/FeatureFlag.java
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;
}
}
}
Loading