Skip to content

Commit 684e47a

Browse files
New audience match types (#213)
* update with some thoughts on how to implement new match types. * add exists * get the lt gt logic correct * suppress unread field in 'exists' match type * nullmatch, substringmatch implementaiton * unit tests * fix Exact to pass back null if the value types are different. added CustomDimensionMatcher to mimic current custom dimension behavior. This should be changed to use Exact. * unit test descriptions * update condition evaluation to return null under matrix given here https://docs.google.com/document/d/158_83difXVXF0nb91rxzrfHZwnhsybH21ImRA_si7sg/edit# * some unit tests (and) * add new match type? * update tests after evaluation of audience with no attributes * bring up test coverage * refactor for java standard 1 class per file. * trying to get lint to pass java 9 * trying to pass findbugs java 9 * just seeing if I can get java 9 to pass again * trying to get travis to pass * i don't know why findbugs is finding problems in java 9 * allow for typedAudiences to exist or not exist in the datafile. * added typed attributes to v4 datafile parse and compare them. Need to write more unit tests that use them * trying to get travis jdk 9 failures * allow null to be returned from evaluate * added unit tests to test AND and OR conditions with null, false, and true * add typed_audience_experiment * added tests using datafile with typed audience with experiment using typed audience. * add exists test. Cleanup code via mike's comments. * add logging, rename custom dimension, clean up code a little * remove unused import * nit cleanup * rename match interface and abstract base class. * refactor to incorporate Nikhil's comments
1 parent 772e7ab commit 684e47a

32 files changed

+1413
-133
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ branches:
2626
- /^\d+\.\d+\.(\d|[x])+(-SNAPSHOT|-alpha|-beta)?\d*$/ # trigger builds on tags which are semantically versioned to ship the SDK.
2727
after_success:
2828
- ./gradlew coveralls uploadArchives --console plain
29+
after_failure:
30+
- cat /home/travis/build/optimizely/java-sdk/core-api/build/reports/findbugs/main.html

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public String toString() {
8080
private final Boolean botFiltering;
8181
private final List<Attribute> attributes;
8282
private final List<Audience> audiences;
83+
private final List<Audience> typedAudiences;
8384
private final List<EventType> events;
8485
private final List<Experiment> experiments;
8586
private final List<FeatureFlag> featureFlags;
@@ -136,6 +137,7 @@ public ProjectConfig(String accountId, String projectId, String version, String
136137
version,
137138
attributes,
138139
audiences,
140+
null,
139141
eventType,
140142
experiments,
141143
null,
@@ -154,6 +156,7 @@ public ProjectConfig(String accountId,
154156
String version,
155157
List<Attribute> attributes,
156158
List<Audience> audiences,
159+
List<Audience> typedAudiences,
157160
List<EventType> events,
158161
List<Experiment> experiments,
159162
List<FeatureFlag> featureFlags,
@@ -170,6 +173,14 @@ public ProjectConfig(String accountId,
170173

171174
this.attributes = Collections.unmodifiableList(attributes);
172175
this.audiences = Collections.unmodifiableList(audiences);
176+
177+
if (typedAudiences != null) {
178+
this.typedAudiences = Collections.unmodifiableList(typedAudiences);
179+
}
180+
else {
181+
this.typedAudiences = Collections.emptyList();
182+
}
183+
173184
this.events = Collections.unmodifiableList(events);
174185
if (featureFlags == null) {
175186
this.featureFlags = Collections.emptyList();
@@ -206,7 +217,14 @@ public ProjectConfig(String accountId,
206217
this.featureKeyMapping = ProjectConfigUtils.generateNameMapping(this.featureFlags);
207218

208219
// generate audience id to audience mapping
209-
this.audienceIdMapping = ProjectConfigUtils.generateIdMapping(audiences);
220+
if (typedAudiences == null) {
221+
this.audienceIdMapping = ProjectConfigUtils.generateIdMapping(audiences);
222+
}
223+
else {
224+
List<Audience> combinedList = new ArrayList<>(audiences);
225+
combinedList.addAll(typedAudiences);
226+
this.audienceIdMapping = ProjectConfigUtils.generateIdMapping(combinedList);
227+
}
210228
this.experimentIdMapping = ProjectConfigUtils.generateIdMapping(this.experiments);
211229
this.groupIdMapping = ProjectConfigUtils.generateIdMapping(groups);
212230
this.rolloutIdMapping = ProjectConfigUtils.generateIdMapping(this.rollouts);
@@ -386,6 +404,10 @@ public List<Audience> getAudiences() {
386404
return audiences;
387405
}
388406

407+
public List<Audience> getTypedAudiences() {
408+
return typedAudiences;
409+
}
410+
389411
public Condition getAudienceConditionsFromId(String audienceId) {
390412
Audience audience = audienceIdMapping.get(audienceId);
391413

@@ -581,6 +603,7 @@ public String toString() {
581603
", botFiltering=" + botFiltering +
582604
", attributes=" + attributes +
583605
", audiences=" + audiences +
606+
", typedAudiences=" + typedAudiences +
584607
", events=" + events +
585608
", experiments=" + experiments +
586609
", featureFlags=" + featureFlags +

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.optimizely.ab.config.audience;
1818

1919
import javax.annotation.Nonnull;
20+
import javax.annotation.Nullable;
2021
import javax.annotation.concurrent.Immutable;
2122
import java.util.List;
2223
import java.util.Map;
@@ -36,13 +37,32 @@ public List<Condition> getConditions() {
3637
return conditions;
3738
}
3839

39-
public boolean evaluate(Map<String, ?> attributes) {
40+
public @Nullable
41+
Boolean evaluate(Map<String, ?> attributes) {
42+
boolean foundNull = false;
43+
// According to the matrix where:
44+
// false and true is false
45+
// false and null is false
46+
// true and null is null.
47+
// true and false is false
48+
// true and true is true
49+
// null and null is null
4050
for (Condition condition : conditions) {
41-
if (!condition.evaluate(attributes))
51+
Boolean conditionEval = condition.evaluate(attributes);
52+
if (conditionEval == null) {
53+
foundNull = true;
54+
}
55+
else if (!conditionEval) { // false with nulls or trues is false.
4256
return false;
57+
}
58+
// true and nulls with no false will be null.
4359
}
4460

45-
return true;
61+
if (foundNull) { // true and null or all null returns null
62+
return null;
63+
}
64+
65+
return true; // otherwise, return true
4666
}
4767

4868
@Override

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,12 +16,14 @@
1616
*/
1717
package com.optimizely.ab.config.audience;
1818

19+
import javax.annotation.Nullable;
1920
import java.util.Map;
2021

2122
/**
2223
* Interface implemented by all conditions condition objects to aid in condition evaluation.
2324
*/
2425
public interface Condition {
2526

26-
boolean evaluate(Map<String, ?> attributes);
27-
}
27+
@Nullable
28+
Boolean evaluate(Map<String, ?> attributes);
29+
}

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

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

19+
import javax.annotation.Nullable;
1920
import javax.annotation.concurrent.Immutable;
2021
import javax.annotation.Nonnull;
2122

@@ -37,8 +38,9 @@ public Condition getCondition() {
3738
return condition;
3839
}
3940

40-
public boolean evaluate(Map<String, ?> attributes) {
41-
return !condition.evaluate(attributes);
41+
public @Nullable Boolean evaluate(Map<String, ?> attributes) {
42+
Boolean conditionEval = condition.evaluate(attributes);
43+
return (conditionEval == null ? null : !conditionEval);
4244
}
4345

4446
@Override

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.optimizely.ab.config.audience;
1818

1919
import javax.annotation.Nonnull;
20+
import javax.annotation.Nullable;
2021
import javax.annotation.concurrent.Immutable;
2122
import java.util.List;
2223
import java.util.Map;
@@ -36,10 +37,26 @@ public List<Condition> getConditions() {
3637
return conditions;
3738
}
3839

39-
public boolean evaluate(Map<String, ?> attributes) {
40+
// According to the matrix:
41+
// true returns true
42+
// false or null is null
43+
// false or false is false
44+
// null or null is null
45+
public @Nullable Boolean evaluate(Map<String, ?> attributes) {
46+
boolean foundNull = false;
4047
for (Condition condition : conditions) {
41-
if (condition.evaluate(attributes))
48+
Boolean conditionEval = condition.evaluate(attributes);
49+
if (conditionEval == null) { // true with falses and nulls is still true
50+
foundNull = true;
51+
}
52+
else if (conditionEval) {
4253
return true;
54+
}
55+
}
56+
57+
// if found null and false return null. all false return false
58+
if (foundNull) {
59+
return null;
4360
}
4461

4562
return false;

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

Lines changed: 20 additions & 10 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.audience.match.MatchType;
20+
1921
import javax.annotation.Nonnull;
2022
import javax.annotation.Nullable;
2123
import javax.annotation.concurrent.Immutable;
@@ -29,11 +31,13 @@ public class UserAttribute implements Condition {
2931

3032
private final String name;
3133
private final String type;
34+
private final String match;
3235
private final Object value;
3336

34-
public UserAttribute(@Nonnull String name, @Nonnull String type, @Nullable Object value) {
37+
public UserAttribute(@Nonnull String name, @Nonnull String type, @Nullable String match, @Nullable Object value) {
3538
this.name = name;
3639
this.type = type;
40+
this.match = match;
3741
this.value = value;
3842
}
3943

@@ -45,25 +49,31 @@ public String getType() {
4549
return type;
4650
}
4751

52+
public String getMatch() {
53+
return match;
54+
}
55+
4856
public Object getValue() {
4957
return value;
5058
}
5159

52-
public boolean evaluate(Map<String, ?> attributes) {
60+
public @Nullable Boolean evaluate(Map<String, ?> attributes) {
5361
// Valid for primative types, but needs to change when a value is an object or an array
5462
Object userAttributeValue = attributes.get(name);
5563

56-
if (value != null) { // if there is a value in the condition
57-
// check user attribute value is equal
58-
return value.equals(userAttributeValue);
64+
if (!"custom_attribute".equals(type)) {
65+
MatchType.logger.error(String.format("condition type not equal to `custom_attribute` %s", type != null ? type : ""));
66+
return null; // unknown type
5967
}
60-
else if (userAttributeValue != null) { // if the datafile value is null but user has a value for this attribute
61-
// return false since null != nonnull
62-
return false;
68+
// check user attribute value is equal
69+
try {
70+
return MatchType.getMatchType(match, value).getMatcher().eval(userAttributeValue);
6371
}
64-
else { // both are null
65-
return true;
72+
catch (NullPointerException np) {
73+
MatchType.logger.error(String.format("attribute or value null for match %s", match != null ? match : "legacy condition"),np);
74+
return null;
6675
}
76+
6777
}
6878

6979
@Override
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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.match;
18+
19+
abstract class AttributeMatch<T> implements Match {
20+
T convert(Object o) {
21+
try {
22+
T rv = (T)o;
23+
return rv;
24+
} catch(java.lang.ClassCastException e) {
25+
MatchType.logger.error(
26+
"Cannot evaluate targeting condition since the value for attribute is an incompatible type",
27+
e
28+
);
29+
return null;
30+
}
31+
}
32+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.match;
18+
19+
import javax.annotation.Nullable;
20+
21+
/**
22+
* This is a temporary class. It mimics the current behaviour for
23+
* legacy custom attributes. This will be dropped for ExactMatch and the unit tests need to be fixed.
24+
* @param <T>
25+
*/
26+
class DefaultMatchForLegacyAttributes<T> extends AttributeMatch<T> {
27+
T value;
28+
protected DefaultMatchForLegacyAttributes(T value) {
29+
this.value = value;
30+
}
31+
32+
public @Nullable
33+
Boolean eval(Object attributeValue) {
34+
return value.equals(convert(attributeValue));
35+
}
36+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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.match;
18+
19+
import javax.annotation.Nullable;
20+
21+
class ExactMatch<T> extends AttributeMatch<T> {
22+
T value;
23+
protected ExactMatch(T value) {
24+
this.value = value;
25+
}
26+
27+
public @Nullable
28+
Boolean eval(Object attributeValue) {
29+
if (!value.getClass().isInstance(attributeValue)) return null;
30+
return value.equals(convert(attributeValue));
31+
}
32+
}

0 commit comments

Comments
 (0)