Skip to content

Commit 3bd8c84

Browse files
Audience combinations (#225)
* create ConditionUtil for condition parsing and add audeince holder. * add ability to parse audienceConditions. Also, consolidate parseConditions to ConditionsUtil. * fix the parsers to parse audienceConditions * add audienceConditions parsed correctly in V4 datafile and empty in the rest * start using audienceConditions * user series of ors to test audienceConditions * rename AudienceHolderCondition to AudienceIdCondition. * add license headers * remove extra parseConditions * add tests for audienceCondition with AND * refactor and move audience id condition resolver to ConditionsUtil * added sanity checks, comments, and exceptions * add exceptions before refactor * use implicit or condition to evaluate * refactor to pass in the project config when evaluating conditions. * remove unused exception * catch all exceptions just in case. * slight refactor * rename methods and cleanup javadoc * update with last fixes * refactor how audienceConditions is parsed * allow for empty audienceConditions. Check for index out of bounds in 'not' operand * change so that empty Not condition returns null * if conditions has no condition for not operator. evaluates to null * refactor out operand parse. rename audience combindations * remove parsing of audienceConditions as string. * add parsing checks for valid and invalid conditions for all parsers. tweak convertObject to use value and attribute to ensure type safety. Add generics for condition types holder * rename convert to castToValueType * add a couple of safeguards * minor fixes from Logan's comments
1 parent 943b151 commit 3bd8c84

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1315
-316
lines changed

core-api/src/main/java/com/optimizely/ab/config/Experiment.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.optimizely.ab.config;
1818

1919
import com.fasterxml.jackson.annotation.*;
20+
import com.optimizely.ab.config.audience.AudienceIdCondition;
21+
import com.optimizely.ab.config.audience.Condition;
2022

2123
import javax.annotation.Nonnull;
2224
import javax.annotation.Nullable;
@@ -41,6 +43,7 @@ public class Experiment implements IdKeyMapped {
4143
private final String groupId;
4244

4345
private final List<String> audienceIds;
46+
private final Condition<AudienceIdCondition> audienceConditions;
4447
private final List<Variation> variations;
4548
private final List<TrafficAllocation> trafficAllocation;
4649

@@ -72,17 +75,19 @@ public Experiment(@JsonProperty("id") String id,
7275
@JsonProperty("status") String status,
7376
@JsonProperty("layerId") String layerId,
7477
@JsonProperty("audienceIds") List<String> audienceIds,
78+
@JsonProperty("audienceConditions") Condition audienceConditions,
7579
@JsonProperty("variations") List<Variation> variations,
7680
@JsonProperty("forcedVariations") Map<String, String> userIdToVariationKeyMap,
7781
@JsonProperty("trafficAllocation") List<TrafficAllocation> trafficAllocation) {
78-
this(id, key, status, layerId, audienceIds, variations, userIdToVariationKeyMap, trafficAllocation, "");
82+
this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "");
7983
}
8084

8185
public Experiment(@Nonnull String id,
8286
@Nonnull String key,
8387
@Nullable String status,
8488
@Nullable String layerId,
8589
@Nonnull List<String> audienceIds,
90+
@Nullable Condition audienceConditions,
8691
@Nonnull List<Variation> variations,
8792
@Nonnull Map<String, String> userIdToVariationKeyMap,
8893
@Nonnull List<TrafficAllocation> trafficAllocation,
@@ -92,6 +97,7 @@ public Experiment(@Nonnull String id,
9297
this.status = status == null ? ExperimentStatus.NOT_STARTED.toString() : status;
9398
this.layerId = layerId;
9499
this.audienceIds = Collections.unmodifiableList(audienceIds);
100+
this.audienceConditions = audienceConditions;
95101
this.variations = Collections.unmodifiableList(variations);
96102
this.trafficAllocation = Collections.unmodifiableList(trafficAllocation);
97103
this.groupId = groupId;
@@ -120,6 +126,10 @@ public List<String> getAudienceIds() {
120126
return audienceIds;
121127
}
122128

129+
public Condition getAudienceConditions() {
130+
return audienceConditions;
131+
}
132+
123133
public List<Variation> getVariations() {
124134
return variations;
125135
}
@@ -165,6 +175,7 @@ public String toString() {
165175
", groupId='" + groupId + '\'' +
166176
", status='" + status + '\'' +
167177
", audienceIds=" + audienceIds +
178+
", audienceConditions=" + audienceConditions +
168179
", variations=" + variations +
169180
", variationKeyToVariationMap=" + variationKeyToVariationMap +
170181
", userIdToVariationKeyMap=" + userIdToVariationKeyMap +

core-api/src/main/java/com/optimizely/ab/config/Group.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public Group(@JsonProperty("id") String id,
5858
experiment.getStatus(),
5959
experiment.getLayerId(),
6060
experiment.getAudienceIds(),
61+
experiment.getAudienceConditions(),
6162
experiment.getVariations(),
6263
experiment.getUserIdToVariationKeyMap(),
6364
experiment.getTrafficAllocation(),

core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import com.optimizely.ab.UnknownEventTypeException;
2121
import com.optimizely.ab.UnknownExperimentException;
2222
import com.optimizely.ab.config.audience.Audience;
23-
import com.optimizely.ab.config.audience.Condition;
2423
import com.optimizely.ab.config.parser.ConfigParseException;
2524
import com.optimizely.ab.config.parser.DefaultConfigParser;
2625
import com.optimizely.ab.error.ErrorHandler;
@@ -408,11 +407,7 @@ public List<Audience> getTypedAudiences() {
408407
return typedAudiences;
409408
}
410409

411-
public Condition getAudienceConditionsFromId(String audienceId) {
412-
Audience audience = audienceIdMapping.get(audienceId);
413-
414-
return audience != null ? audience.getConditions() : null;
415-
}
410+
public Audience getAudience(String audienceId) { return audienceIdMapping.get(audienceId); }
416411

417412
public List<LiveVariable> getLiveVariables() {
418413
return liveVariables;

core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package com.optimizely.ab.config.audience;
1818

19+
import com.optimizely.ab.config.ProjectConfig;
20+
1921
import javax.annotation.Nonnull;
2022
import javax.annotation.Nullable;
2123
import javax.annotation.concurrent.Immutable;
@@ -25,8 +27,7 @@
2527
/**
2628
* Represents an 'And' conditions condition operation.
2729
*/
28-
@Immutable
29-
public class AndCondition implements Condition {
30+
public class AndCondition<T> implements Condition<T> {
3031

3132
private final List<Condition> conditions;
3233
public AndCondition(@Nonnull List<Condition> conditions) {
@@ -38,7 +39,8 @@ public List<Condition> getConditions() {
3839
}
3940

4041
public @Nullable
41-
Boolean evaluate(Map<String, ?> attributes) {
42+
Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
43+
if (conditions == null) return null;
4244
boolean foundNull = false;
4345
// According to the matrix where:
4446
// false and true is false
@@ -48,7 +50,7 @@ Boolean evaluate(Map<String, ?> attributes) {
4850
// true and true is true
4951
// null and null is null
5052
for (Condition condition : conditions) {
51-
Boolean conditionEval = condition.evaluate(attributes);
53+
Boolean conditionEval = condition.evaluate(config, attributes);
5254
if (conditionEval == null) {
5355
foundNull = true;
5456
}

core-api/src/main/java/com/optimizely/ab/config/audience/Audience.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class Audience implements IdKeyMapped {
3434

3535
private final String id;
3636
private final String name;
37-
private final Condition conditions;
37+
private final Condition<UserAttribute> conditions;
3838

3939
@JsonCreator
4040
public Audience(@JsonProperty("id") String id,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
*
3+
* Copyright 2018, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.config.audience;
18+
19+
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import com.optimizely.ab.config.ProjectConfig;
23+
import com.optimizely.ab.internal.InvalidAudienceCondition;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
27+
import javax.annotation.Nullable;
28+
import javax.annotation.concurrent.Immutable;
29+
import java.util.Map;
30+
import java.util.Objects;
31+
32+
/**
33+
* The AudienceIdCondition is a holder for the audience id in
34+
* {@link com.optimizely.ab.config.Experiment#audienceConditions auienceConditions}.
35+
* If the audienceId is not resolved at evaluation time, the
36+
* condition will fail. AudienceIdConditions are resolved when the ProjectConfig is passed into evaluate.
37+
*/
38+
@JsonIgnoreProperties(ignoreUnknown = true)
39+
public class AudienceIdCondition<T> implements Condition<T> {
40+
private Audience audience;
41+
final private String audienceId;
42+
43+
final private static Logger logger = LoggerFactory.getLogger("AudienceIdCondition");
44+
45+
/**
46+
* Constructor used in json parsing to store the audienceId parsed from Experiment.audienceConditions.
47+
* @param audienceId
48+
*/
49+
@JsonCreator
50+
public AudienceIdCondition(String audienceId) {
51+
this.audienceId = audienceId;
52+
}
53+
54+
public Audience getAudience() {
55+
return audience;
56+
}
57+
58+
public void setAudience(Audience audience) {
59+
this.audience = audience;
60+
}
61+
62+
public String getAudienceId() {
63+
return audienceId;
64+
}
65+
66+
@Nullable
67+
@Override
68+
public Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
69+
if (config != null) {
70+
audience = config.getAudienceIdMapping().get(audienceId);
71+
}
72+
if (audience == null) {
73+
logger.error(String.format("Audience not set for audienceConditions %s", audienceId));
74+
return null;
75+
}
76+
return audience.getConditions().evaluate(config, attributes);
77+
}
78+
79+
@Override
80+
public boolean equals(Object o) {
81+
if (this == o) return true;
82+
if (o == null || getClass() != o.getClass()) return false;
83+
AudienceIdCondition condition = (AudienceIdCondition) o;
84+
return ((audience == null) ? (null == condition.audience) :
85+
(audience.getId().equals(condition.audience!=null?condition.audience.getId():null))) &&
86+
((audienceId == null) ? (null == condition.audienceId) :
87+
(audienceId.equals(condition.audienceId)));
88+
}
89+
90+
@Override
91+
public int hashCode() {
92+
93+
return Objects.hash(audience, audienceId);
94+
}
95+
96+
@Override
97+
public String toString() {
98+
return audienceId;
99+
}
100+
}

core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616
*/
1717
package com.optimizely.ab.config.audience;
1818

19+
import com.optimizely.ab.config.ProjectConfig;
20+
1921
import javax.annotation.Nullable;
2022
import java.util.Map;
2123

2224
/**
2325
* Interface implemented by all conditions condition objects to aid in condition evaluation.
2426
*/
25-
public interface Condition {
27+
public interface Condition<T> {
2628

2729
@Nullable
28-
Boolean evaluate(Map<String, ?> attributes);
30+
Boolean evaluate(ProjectConfig config, Map<String, ?> attributes);
2931
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.optimizely.ab.config.audience;
2+
3+
import com.optimizely.ab.config.ProjectConfig;
4+
5+
import javax.annotation.Nullable;
6+
import java.util.Map;
7+
8+
public class EmptyCondition<T> implements Condition<T> {
9+
@Nullable
10+
@Override
11+
public Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
12+
return true;
13+
}
14+
15+
}

core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package com.optimizely.ab.config.audience;
1818

19+
import com.optimizely.ab.config.ProjectConfig;
20+
1921
import javax.annotation.Nullable;
2022
import javax.annotation.concurrent.Immutable;
2123
import javax.annotation.Nonnull;
@@ -26,7 +28,7 @@
2628
* Represents a 'Not' conditions condition operation.
2729
*/
2830
@Immutable
29-
public class NotCondition implements Condition {
31+
public class NotCondition<T> implements Condition<T> {
3032

3133
private final Condition condition;
3234

@@ -38,8 +40,9 @@ public Condition getCondition() {
3840
return condition;
3941
}
4042

41-
public @Nullable Boolean evaluate(Map<String, ?> attributes) {
42-
Boolean conditionEval = condition.evaluate(attributes);
43+
public @Nullable Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
44+
45+
Boolean conditionEval = condition == null ? null : condition.evaluate(config, attributes);
4346
return (conditionEval == null ? null : !conditionEval);
4447
}
4548

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.optimizely.ab.config.audience;
2+
3+
import com.optimizely.ab.config.ProjectConfig;
4+
5+
import javax.annotation.Nullable;
6+
import java.util.Map;
7+
8+
public class NullCondition<T> implements Condition<T> {
9+
@Nullable
10+
@Override
11+
public Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
12+
return null;
13+
}
14+
}

0 commit comments

Comments
 (0)