Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 60 additions & 5 deletions src/main/java/co/featbit/server/DataModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public abstract class DataModel {
Expand Down Expand Up @@ -172,6 +175,8 @@ static class Segment implements DataStorageTypes.Item, JsonHelper.AfterJsonParse
private final List<String> included;
private final List<String> excluded;
private final List<TargetRule> rules;
private transient Set<String> includedSet;
private transient Set<String> excludedSet;

Segment(String id, Boolean isArchived, Date updatedAt, List<String> included, List<String> excluded, List<TargetRule> rules) {
this.id = id;
Expand Down Expand Up @@ -215,24 +220,34 @@ public List<TargetRule> getRules() {
}

public Boolean isMatchUser(String userKeyId) {
if (getExcluded().contains(userKeyId)) {
if (excludedSet().contains(userKeyId)) {
return Boolean.FALSE;
}

if (getIncluded().contains(userKeyId)) {
if (includedSet().contains(userKeyId)) {
return Boolean.TRUE;
}
// if no included or excluded, then it's to match rules
return null;
}

private Set<String> includedSet() {
return includedSet == null ? toSet(getIncluded()) : includedSet;
}

private Set<String> excludedSet() {
return excludedSet == null ? toSet(getExcluded()) : excludedSet;
}

public DataStorageTypes.Item toArchivedItem() {
return new ArchivedItem(this.id, this.timestamp);
}

@Override
public void afterDeserialization() {
this.timestamp = updatedAt.getTime();
this.includedSet = toSet(getIncluded());
this.excludedSet = toSet(getExcluded());
}

@Override
Expand Down Expand Up @@ -391,11 +406,14 @@ public String getValue() {
}
}

static final class TargetUser {
@JsonAdapter(JsonHelper.AfterJsonParseDeserializableTypeAdapterFactory.class)
static final class TargetUser implements JsonHelper.AfterJsonParseDeserializable {
private final List<String> keyIds;

private final String variationId;

private transient Set<String> keyIdSet;

public TargetUser(List<String> keyIds, String variationId) {
this.keyIds = keyIds;
this.variationId = variationId;
Expand All @@ -410,7 +428,12 @@ public String getVariationId() {
}

public boolean isTargeted(String user) {
return getKeyIds().stream().anyMatch(key -> key.equals(user));
return (keyIdSet == null ? toSet(getKeyIds()) : keyIdSet).contains(user);
}

@Override
public void afterDeserialization() {
this.keyIdSet = toSet(getKeyIds());
}
}

Expand Down Expand Up @@ -447,13 +470,17 @@ public String getDispatchKey() {
}
}

static final class Condition {
@JsonAdapter(JsonHelper.AfterJsonParseDeserializableTypeAdapterFactory.class)
static final class Condition implements JsonHelper.AfterJsonParseDeserializable {
private final String property;

private final String op;

private final String value;

// the parsed value of a list operator, built once per sync; null when the value is not a list
private transient Collection<String> parsedValues;

Condition(String property, String op, String value) {
this.property = property;
this.op = op;
Expand All @@ -471,6 +498,34 @@ public String getOp() {
public String getValue() {
return value;
}

Collection<String> getParsedValues() {
return parsedValues;
}

@Override
public void afterDeserialization() {
// segment conditions have no op, see EvaluatorImp.ifUserMatchClause
String clause = StringUtils.isBlank(op) ? property : op;
boolean oneOf = Evaluator.IS_ONE_OF_CLAUSE.equals(clause) || Evaluator.NOT_ONE_OF_CLAUSE.equals(clause);
boolean inSegment = Evaluator.IS_IN_SEGMENT_CLAUSE.equals(clause) || Evaluator.NOT_IN_SEGMENT_CLAUSE.equals(clause);
if (!oneOf && !inSegment) {
return;
}
try {
List<String> values = JsonHelper.deserialize(value, new TypeToken<List<String>>() {
}.getType());
// segments are checked in the listed order and evaluation stops at the first match, so keep that order
parsedValues = values == null ? null : oneOf ? toSet(values) : Collections.unmodifiableList(values);
} catch (RuntimeException e) {
// leave it to evaluation, which keeps the original behavior for an invalid value
parsedValues = null;
}
}
}

private static Set<String> toSet(List<String> values) {
return Collections.unmodifiableSet(new HashSet<>(values));
}

static final class RolloutVariation {
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/co/featbit/server/EvaluatorImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,11 @@ private boolean ifUserMatchClause(FBUser user, DataModel.Condition condition) {
private boolean inSegmentClause(FBUser user, DataModel.Condition condition) {
String pv = user.getKey();
try {
List<String> segments = JsonHelper.deserialize(condition.getValue(), new TypeToken<List<String>>() {
}.getType());
Collection<String> segments = condition.getParsedValues();
if (segments == null) {
segments = JsonHelper.deserialize(condition.getValue(), new TypeToken<List<String>>() {
}.getType());
}
return segments.stream().map(segmentGetter::get).anyMatch(segment -> {
if (segment == null) {
return false;
Expand Down Expand Up @@ -223,8 +226,11 @@ private boolean containsClause(FBUser user, DataModel.Condition condition) {
private boolean oneOfClause(FBUser user, DataModel.Condition condition) {
String pv = user.getProperty(condition.getProperty());
try {
List<String> clauseValues = JsonHelper.deserialize(condition.getValue(), new TypeToken<List<String>>() {
}.getType());
Collection<String> clauseValues = condition.getParsedValues();
if (clauseValues == null) {
clauseValues = JsonHelper.deserialize(condition.getValue(), new TypeToken<List<String>>() {
}.getType());
}
return pv != null && clauseValues.contains(pv);
} catch (JsonParseException e) {
return false;
Expand Down
Loading
Loading