diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java index be1e11169..277f2f184 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java @@ -20,10 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.config.audience.match.Match; -import com.optimizely.ab.config.audience.match.MatchType; -import com.optimizely.ab.config.audience.match.UnexpectedValueTypeException; -import com.optimizely.ab.config.audience.match.UnknownMatchTypeException; +import com.optimizely.ab.config.audience.match.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -87,35 +84,36 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { } // check user attribute value is equal try { - Match matchType = MatchType.getMatchType(match, value).getMatcher(); - Boolean result = matchType.eval(userAttributeValue); - + Match matcher = MatchRegistry.getMatch(match); + Boolean result = matcher.eval(value, userAttributeValue); if (result == null) { - if (!attributes.containsKey(name)) { - //Missing attribute value - logger.debug("Audience condition \"{}\" evaluated to UNKNOWN because no value was passed for user attribute \"{}\"", this, name); + throw new UnknownValueTypeException(); + } + + return result; + } catch(UnknownValueTypeException e) { + if (!attributes.containsKey(name)) { + //Missing attribute value + logger.debug("Audience condition \"{}\" evaluated to UNKNOWN because no value was passed for user attribute \"{}\"", this, name); + } else { + //if attribute value is not valid + if (userAttributeValue != null) { + logger.warn( + "Audience condition \"{}\" evaluated to UNKNOWN because a value of type \"{}\" was passed for user attribute \"{}\"", + this, + userAttributeValue.getClass().getCanonicalName(), + name); } else { - //if attribute value is not valid - if (userAttributeValue != null) { - logger.warn( - "Audience condition \"{}\" evaluated to UNKNOWN because a value of type \"{}\" was passed for user attribute \"{}\"", - this, - userAttributeValue.getClass().getCanonicalName(), - name); - } else { - logger.debug( - "Audience condition \"{}\" evaluated to UNKNOWN because a null value was passed for user attribute \"{}\"", - this, - name); - } + logger.debug( + "Audience condition \"{}\" evaluated to UNKNOWN because a null value was passed for user attribute \"{}\"", + this, + name); } } - return result; - } catch (UnknownMatchTypeException | UnexpectedValueTypeException ex) { - logger.warn("Audience condition \"{}\" " + ex.getMessage(), - this); - } catch (NullPointerException np) { - logger.error("attribute or value null for match {}", match != null ? match : "legacy condition", np); + } catch (UnknownMatchTypeException | UnexpectedValueTypeException e) { + logger.warn("Audience condition \"{}\" " + e.getMessage(), this); + } catch (NullPointerException e) { + logger.error("attribute or value null for match {}", match != null ? match : "legacy condition", e); } return null; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/DefaultMatchForLegacyAttributes.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/DefaultMatchForLegacyAttributes.java index cb2dfa671..c3c970541 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/DefaultMatchForLegacyAttributes.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/DefaultMatchForLegacyAttributes.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2018-2020, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,17 +21,16 @@ /** * This is a temporary class. It mimics the current behaviour for * legacy custom attributes. This will be dropped for ExactMatch and the unit tests need to be fixed. - * @param */ -class DefaultMatchForLegacyAttributes extends AttributeMatch { - T value; - - protected DefaultMatchForLegacyAttributes(T value) { - this.value = value; - } - +class DefaultMatchForLegacyAttributes implements Match { @Nullable - public Boolean eval(Object attributeValue) { - return value.equals(castToValueType(attributeValue, value)); + public Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException { + if (!(conditionValue instanceof String)) { + throw new UnexpectedValueTypeException(); + } + if (attributeValue == null) { + return false; + } + return conditionValue.toString().equals(attributeValue.toString()); } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExactMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExactMatch.java index 9d8d4e8c3..5781ac892 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExactMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExactMatch.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2018-2020, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,18 +18,31 @@ import javax.annotation.Nullable; -class ExactMatch extends AttributeMatch { - T value; - - protected ExactMatch(T value) { - this.value = value; - } +import static com.optimizely.ab.internal.AttributesUtil.isValidNumber; +/** + * ExactMatch supports matching Numbers, Strings and Booleans. Numbers are first converted to doubles + * before the comparison is evaluated. See {@link NumberComparator} Strings and Booleans are evaulated + * via the Object equals method. + */ +class ExactMatch implements Match { @Nullable - public Boolean eval(Object attributeValue) { - T converted = castToValueType(attributeValue, value); - if (value != null && converted == null) return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException { + if (isValidNumber(attributeValue)) { + if (isValidNumber(conditionValue)) { + return NumberComparator.compareUnsafe(attributeValue, conditionValue) == 0; + } + return null; + } + + if (!(conditionValue instanceof String || conditionValue instanceof Boolean)) { + throw new UnexpectedValueTypeException(); + } + + if (attributeValue == null || attributeValue.getClass() != conditionValue.getClass()) { + return null; + } - return value == null ? attributeValue == null : value.equals(converted); + return conditionValue.equals(attributeValue); } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExactNumberMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExactNumberMatch.java deleted file mode 100644 index 56984537a..000000000 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExactNumberMatch.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * - * Copyright 2018-2019, Optimizely and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.optimizely.ab.config.audience.match; - -import com.optimizely.ab.config.ProjectConfig; - -import javax.annotation.Nullable; - -import static com.optimizely.ab.internal.AttributesUtil.isValidNumber; - -// Because json number is a double in most java json parsers. at this -// point we allow comparision of Integer and Double. The instance class is Double and -// Integer which would fail in our normal exact match. So, we are special casing for now. We have already filtered -// out other Number types. -public class ExactNumberMatch extends AttributeMatch { - Number value; - - protected ExactNumberMatch(Number value) { - this.value = value; - } - - @Nullable - public Boolean eval(Object attributeValue) { - try { - if(isValidNumber(attributeValue)) { - return value.doubleValue() == castToValueType(attributeValue, value).doubleValue(); - } - } catch (Exception e) { - } - - return null; - } -} diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExistsMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExistsMatch.java index 594ea6fc4..38fb5a884 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExistsMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/ExistsMatch.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2018-2020, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,14 @@ */ package com.optimizely.ab.config.audience.match; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - import javax.annotation.Nullable; +/** + * ExistsMatch checks that the attribute value is NOT null. + */ class ExistsMatch implements Match { - @SuppressFBWarnings("URF_UNREAD_FIELD") - Object value; - - protected ExistsMatch(Object value) { - this.value = value; - } - @Nullable - public Boolean eval(Object attributeValue) { + public Boolean eval(Object conditionValue, Object attributeValue) { return attributeValue != null; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/GEMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/GEMatch.java index 8724cfcb0..e66012cba 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/GEMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/GEMatch.java @@ -18,24 +18,12 @@ import javax.annotation.Nullable; -import static com.optimizely.ab.internal.AttributesUtil.isValidNumber; - -class GEMatch extends AttributeMatch { - Number value; - - protected GEMatch(Number value) { - this.value = value; - } - +/** + * GEMatch performs a "greater than or equal to" number comparison via {@link NumberComparator}. + */ +class GEMatch implements Match { @Nullable - public Boolean eval(Object attributeValue) { - try { - if(isValidNumber(attributeValue)) { - return castToValueType(attributeValue, value).doubleValue() >= value.doubleValue(); - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnknownValueTypeException { + return NumberComparator.compare(attributeValue, conditionValue) >= 0; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/GTMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/GTMatch.java index 8b9e9dd7b..ba6689c9e 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/GTMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/GTMatch.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2018-2020, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,24 +18,12 @@ import javax.annotation.Nullable; -import static com.optimizely.ab.internal.AttributesUtil.isValidNumber; - -class GTMatch extends AttributeMatch { - Number value; - - protected GTMatch(Number value) { - this.value = value; - } - +/** + * GTMatch performs a "greater than" number comparison via {@link NumberComparator}. + */ +class GTMatch implements Match { @Nullable - public Boolean eval(Object attributeValue) { - try { - if(isValidNumber(attributeValue)) { - return castToValueType(attributeValue, value).doubleValue() > value.doubleValue(); - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnknownValueTypeException { + return NumberComparator.compare(attributeValue, conditionValue) > 0; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/LEMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/LEMatch.java index 23d1c03fc..b222fa022 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/LEMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/LEMatch.java @@ -18,25 +18,13 @@ import javax.annotation.Nullable; -import static com.optimizely.ab.internal.AttributesUtil.isValidNumber; - -class LEMatch extends AttributeMatch { - Number value; - - protected LEMatch(Number value) { - this.value = value; - } - +/** + * GEMatch performs a "less than or equal to" number comparison via {@link NumberComparator}. + */ +class LEMatch implements Match { @Nullable - public Boolean eval(Object attributeValue) { - try { - if(isValidNumber(attributeValue)) { - return castToValueType(attributeValue, value).doubleValue() <= value.doubleValue(); - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnknownValueTypeException { + return NumberComparator.compare(attributeValue, conditionValue) <= 0; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/LTMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/LTMatch.java index 951adbffb..3000aedff 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/LTMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/LTMatch.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2018-2020, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,25 +18,12 @@ import javax.annotation.Nullable; -import static com.optimizely.ab.internal.AttributesUtil.isValidNumber; - -class LTMatch extends AttributeMatch { - Number value; - - protected LTMatch(Number value) { - this.value = value; - } - +/** + * GTMatch performs a "less than" number comparison via {@link NumberComparator}. + */ +class LTMatch implements Match { @Nullable - public Boolean eval(Object attributeValue) { - try { - if(isValidNumber(attributeValue)) { - return castToValueType(attributeValue, value).doubleValue() < value.doubleValue(); - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnknownValueTypeException { + return NumberComparator.compare(attributeValue, conditionValue) < 0; } } - diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/Match.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/Match.java index 2f0d3a2a1..7bef74e6c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/Match.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/Match.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2018-2020, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,5 +20,5 @@ public interface Match { @Nullable - Boolean eval(Object attributeValue); + Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException, UnknownValueTypeException; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/MatchRegistry.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/MatchRegistry.java new file mode 100644 index 000000000..a468bc5e2 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/MatchRegistry.java @@ -0,0 +1,82 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config.audience.match; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * MatchRegistry maps a string match "type" to a match implementation. + * All supported Match implementations must be registed with this registry. + * Third-party {@link Match} implementations may also be registered to provide + * additional functionality. + */ +public class MatchRegistry { + + private static final Map registry = new ConcurrentHashMap<>(); + public static final String EXACT = "exact"; + public static final String EXISTS = "exists"; + public static final String GREATER_THAN = "gt"; + public static final String GREATER_THAN_EQ = "ge"; + public static final String LEGACY = "legacy"; + public static final String LESS_THAN = "lt"; + public static final String LESS_THAN_EQ = "le"; + public static final String SEMVER_EQ = "semver_eq"; + public static final String SEMVER_GE = "semver_ge"; + public static final String SEMVER_GT = "semver_gt"; + public static final String SEMVER_LE = "semver_le"; + public static final String SEMVER_LT = "semver_lt"; + public static final String SUBSTRING = "substring"; + + static { + register(EXACT, new ExactMatch()); + register(EXISTS, new ExistsMatch()); + register(GREATER_THAN, new GTMatch()); + register(GREATER_THAN_EQ, new GEMatch()); + register(LEGACY, new DefaultMatchForLegacyAttributes()); + register(LESS_THAN, new LTMatch()); + register(LESS_THAN_EQ, new LEMatch()); + register(SEMVER_EQ, new SemanticVersionEqualsMatch()); + register(SEMVER_GE, new SemanticVersionGEMatch()); + register(SEMVER_GT, new SemanticVersionGTMatch()); + register(SEMVER_LE, new SemanticVersionLEMatch()); + register(SEMVER_LT, new SemanticVersionLTMatch()); + register(SUBSTRING, new SubstringMatch()); + } + + // TODO rename Match to Matcher + public static Match getMatch(String name) throws UnknownMatchTypeException { + Match match = registry.get(name == null ? LEGACY : name); + if (match == null) { + throw new UnknownMatchTypeException(); + } + + return match; + } + + /** + * register registers a Match implementation with it's name. + * NOTE: This does not check for existence so default implementations can + * be overridden. + * @param name + * @param match + */ + public static void register(String name, Match match) { + registry.put(name, match); + } + +} diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/MatchType.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/MatchType.java deleted file mode 100644 index 7455f1270..000000000 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/MatchType.java +++ /dev/null @@ -1,124 +0,0 @@ -/** - * - * Copyright 2018-2020, Optimizely and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.optimizely.ab.config.audience.match; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nonnull; - -import static com.optimizely.ab.internal.AttributesUtil.isValidNumber; - -public class MatchType { - - public static final Logger logger = LoggerFactory.getLogger(MatchType.class); - - private String matchType; - private Match matcher; - - public static MatchType getMatchType(String matchType, Object conditionValue) throws UnexpectedValueTypeException, UnknownMatchTypeException { - if (matchType == null) matchType = "legacy_custom_attribute"; - - switch (matchType) { - case "exists": - return new MatchType(matchType, new ExistsMatch(conditionValue)); - case "exact": - if (conditionValue instanceof String) { - return new MatchType(matchType, new ExactMatch((String) conditionValue)); - } else if (isValidNumber(conditionValue)) { - return new MatchType(matchType, new ExactNumberMatch((Number) conditionValue)); - } else if (conditionValue instanceof Boolean) { - return new MatchType(matchType, new ExactMatch((Boolean) conditionValue)); - } - break; - case "substring": - if (conditionValue instanceof String) { - return new MatchType(matchType, new SubstringMatch((String) conditionValue)); - } - break; - case "ge": - if (isValidNumber(conditionValue)) { - return new MatchType(matchType, new GEMatch((Number) conditionValue)); - } - break; - case "gt": - if (isValidNumber(conditionValue)) { - return new MatchType(matchType, new GTMatch((Number) conditionValue)); - } - break; - case "le": - if (isValidNumber(conditionValue)) { - return new MatchType(matchType, new LEMatch((Number) conditionValue)); - } - break; - case "lt": - if (isValidNumber(conditionValue)) { - return new MatchType(matchType, new LTMatch((Number) conditionValue)); - } - break; - case "legacy_custom_attribute": - if (conditionValue instanceof String) { - return new MatchType(matchType, new DefaultMatchForLegacyAttributes((String) conditionValue)); - } - break; - case "semver_eq": - if (conditionValue instanceof String) { - return new MatchType(matchType, new SemanticVersionEqualsMatch((String) conditionValue)); - } - break; - case "semver_ge": - if (conditionValue instanceof String) { - return new MatchType(matchType, new SemanticVersionGEMatch((String) conditionValue)); - } - break; - case "semver_gt": - if (conditionValue instanceof String) { - return new MatchType(matchType, new SemanticVersionGTMatch((String) conditionValue)); - } - break; - case "semver_le": - if (conditionValue instanceof String) { - return new MatchType(matchType, new SemanticVersionLEMatch((String) conditionValue)); - } - break; - case "semver_lt": - if (conditionValue instanceof String) { - return new MatchType(matchType, new SemanticVersionLTMatch((String) conditionValue)); - } - break; - default: - throw new UnknownMatchTypeException(); - } - - throw new UnexpectedValueTypeException(); - } - - private MatchType(String type, Match matcher) { - this.matchType = type; - this.matcher = matcher; - } - - @Nonnull - public Match getMatcher() { - return matcher; - } - - @Override - public String toString() { - return matchType; - } -} diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/NumberComparator.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/NumberComparator.java new file mode 100644 index 000000000..49ce94eab --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/NumberComparator.java @@ -0,0 +1,41 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config.audience.match; + +import static com.optimizely.ab.internal.AttributesUtil.isValidNumber; + +/** + * NumberComparator performs a numeric comparison. The input values are assumed to be numbers else + * compare will throw an {@link UnknownValueTypeException}. + */ +public class NumberComparator { + public static int compare(Object o1, Object o2) throws UnknownValueTypeException { + if (!isValidNumber(o1) || !isValidNumber(o2)) { + throw new UnknownValueTypeException(); + } + + return compareUnsafe(o1, o2); + } + + /** + * compareUnsafe is provided to avoid checking the input values are numbers. It's assumed that the inputs + * are known to be Numbers. + */ + static int compareUnsafe(Object o1, Object o2) { + return Double.compare(((Number) o1).doubleValue(), ((Number) o2).doubleValue()); + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersion.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersion.java index 9e37ac02b..d963e7702 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersion.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersion.java @@ -16,6 +16,9 @@ */ package com.optimizely.ab.config.audience.match; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -23,8 +26,12 @@ import static com.optimizely.ab.internal.AttributesUtil.parseNumeric; import static com.optimizely.ab.internal.AttributesUtil.stringIsNullOrEmpty; +/** + * SemanticVersion implements the specification for the purpose of comparing two Versions. + */ public final class SemanticVersion { + private static final Logger logger = LoggerFactory.getLogger(SemanticVersion.class); private static final String BUILD_SEPERATOR = "\\+"; private static final String PRE_RELEASE_SEPERATOR = "-"; @@ -34,6 +41,24 @@ public SemanticVersion(String version) { this.version = version; } + /** + * compare takes object inputs and coerces them into SemanticVersion objects before performing the comparison. + * If the input values cannot be coerced then an {@link UnexpectedValueTypeException} is thrown. + */ + public static int compare(Object o1, Object o2) throws UnexpectedValueTypeException { + if (o1 instanceof String && o2 instanceof String) { + SemanticVersion v1 = new SemanticVersion((String) o1); + SemanticVersion v2 = new SemanticVersion((String) o2); + try { + return v1.compare(v2); + } catch (Exception e) { + logger.warn("Error comparing semantic versions", e); + } + } + + throw new UnexpectedValueTypeException(); + } + public int compare(SemanticVersion targetedVersion) throws Exception { if (targetedVersion == null || stringIsNullOrEmpty(targetedVersion.version)) { diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionEqualsMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionEqualsMatch.java index b727d88cf..ac0c8310b 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionEqualsMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionEqualsMatch.java @@ -18,24 +18,12 @@ import javax.annotation.Nullable; +/** + * SemanticVersionEqualsMatch performs a equality comparison via {@link SemanticVersion#compare(Object, Object)}. + */ class SemanticVersionEqualsMatch implements Match { - String value; - - protected SemanticVersionEqualsMatch(String value) { - this.value = value; - } - @Nullable - public Boolean eval(Object attributeValue) { - try { - if (this.value != null && attributeValue instanceof String) { - SemanticVersion conditionalVersion = new SemanticVersion(value); - SemanticVersion userSemanticVersion = new SemanticVersion((String) attributeValue); - return userSemanticVersion.compare(conditionalVersion) == 0; - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException { + return SemanticVersion.compare(attributeValue, conditionValue) == 0; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionGEMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionGEMatch.java index fd31e1ab2..91f95d4cd 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionGEMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionGEMatch.java @@ -18,24 +18,13 @@ import javax.annotation.Nullable; +/** + * SemanticVersionGEMatch performs a "greater than or equal to" comparison + * via {@link SemanticVersion#compare(Object, Object)}. + */ class SemanticVersionGEMatch implements Match { - String value; - - protected SemanticVersionGEMatch(String value) { - this.value = value; - } - @Nullable - public Boolean eval(Object attributeValue) { - try { - if (this.value != null && attributeValue instanceof String) { - SemanticVersion conditionalVersion = new SemanticVersion(value); - SemanticVersion userSemanticVersion = new SemanticVersion((String) attributeValue); - return userSemanticVersion.compare(conditionalVersion) >= 0; - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException { + return SemanticVersion.compare(attributeValue, conditionValue) >= 0; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionGTMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionGTMatch.java index 7ca0f31b1..52513024c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionGTMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionGTMatch.java @@ -18,24 +18,12 @@ import javax.annotation.Nullable; +/** + * SemanticVersionGTMatch performs a "greater than" comparison via {@link SemanticVersion#compare(Object, Object)}. + */ class SemanticVersionGTMatch implements Match { - String value; - - protected SemanticVersionGTMatch(String target) { - this.value = target; - } - @Nullable - public Boolean eval(Object attributeValue) { - try { - if (this.value != null && attributeValue instanceof String) { - SemanticVersion conditionalVersion = new SemanticVersion(value); - SemanticVersion userSemanticVersion = new SemanticVersion((String) attributeValue); - return userSemanticVersion.compare(conditionalVersion) > 0; - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException { + return SemanticVersion.compare(attributeValue, conditionValue) > 0; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionLEMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionLEMatch.java index 6c7629672..4297d4545 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionLEMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionLEMatch.java @@ -18,24 +18,13 @@ import javax.annotation.Nullable; +/** + * SemanticVersionLEMatch performs a "less than or equal to" comparison + * via {@link SemanticVersion#compare(Object, Object)}. + */ class SemanticVersionLEMatch implements Match { - String value; - - protected SemanticVersionLEMatch(String target) { - this.value = target; - } - @Nullable - public Boolean eval(Object attributeValue) { - try { - if (this.value != null && attributeValue instanceof String) { - SemanticVersion conditionalVersion = new SemanticVersion(value); - SemanticVersion userSemanticVersion = new SemanticVersion((String) attributeValue); - return userSemanticVersion.compare(conditionalVersion) <= 0; - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException { + return SemanticVersion.compare(attributeValue, conditionValue) <= 0; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionLTMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionLTMatch.java index 6f67863a1..a35dcd2da 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionLTMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SemanticVersionLTMatch.java @@ -18,24 +18,12 @@ import javax.annotation.Nullable; +/** + * SemanticVersionLTMatch performs a "less than" comparison via {@link SemanticVersion#compare(Object, Object)}. + */ class SemanticVersionLTMatch implements Match { - String value; - - protected SemanticVersionLTMatch(String target) { - this.value = target; - } - @Nullable - public Boolean eval(Object attributeValue) { - try { - if (this.value != null && attributeValue instanceof String) { - SemanticVersion conditionalVersion = new SemanticVersion(value); - SemanticVersion userSemanticVersion = new SemanticVersion((String) attributeValue); - return userSemanticVersion.compare(conditionalVersion) < 0; - } - } catch (Exception e) { - return null; - } - return null; + public Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException { + return SemanticVersion.compare(attributeValue, conditionValue) < 0; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SubstringMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SubstringMatch.java index 946ebad99..5a573e495 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/SubstringMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/SubstringMatch.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2018-2020, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,23 +18,23 @@ import javax.annotation.Nullable; -class SubstringMatch extends AttributeMatch { - String value; +/** + * SubstringMatch checks if the attribute value contains the condition value. + * This assumes both the condition and attribute values are provided as Strings. + */ +class SubstringMatch implements Match { + @Nullable + public Boolean eval(Object conditionValue, Object attributeValue) throws UnexpectedValueTypeException { + if (!(conditionValue instanceof String)) { + throw new UnexpectedValueTypeException(); + } - protected SubstringMatch(String value) { - this.value = value; - } + if (!(attributeValue instanceof String)) { + return null; + } - /** - * This matches the same substring matching logic in the Web client. - * - * @param attributeValue - * @return true/false if the user attribute string value contains the condition string value - */ - @Nullable - public Boolean eval(Object attributeValue) { try { - return castToValueType(attributeValue, value).contains(value); + return attributeValue.toString().contains(conditionValue.toString()); } catch (Exception e) { return null; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnexpectedValueTypeException.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnexpectedValueTypeException.java index cf513bc7d..39cde7a21 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnexpectedValueTypeException.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnexpectedValueTypeException.java @@ -17,6 +17,10 @@ package com.optimizely.ab.config.audience.match; +/** + * UnexpectedValueTypeException is thrown when the condition value found in the datafile is + * not one of an expected type for this version of the SDK. + */ public class UnexpectedValueTypeException extends Exception { private static String message = "has an unsupported condition value. You may need to upgrade to a newer release of the Optimizely SDK."; diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownMatchTypeException.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownMatchTypeException.java index 0c5a972a7..1f371586b 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownMatchTypeException.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownMatchTypeException.java @@ -17,6 +17,9 @@ package com.optimizely.ab.config.audience.match; +/** + * UnknownMatchTypeException is thrown when the specified match type cannot be mapped via the MatchRegistry. + */ public class UnknownMatchTypeException extends Exception { private static String message = "uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK."; diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/AttributeMatch.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownValueTypeException.java similarity index 60% rename from core-api/src/main/java/com/optimizely/ab/config/audience/match/AttributeMatch.java rename to core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownValueTypeException.java index e2f413c4e..6df4ef1e1 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/AttributeMatch.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownValueTypeException.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2020, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,20 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.optimizely.ab.config.audience.match; -abstract class AttributeMatch implements Match { - T castToValueType(Object o, Object value) { - try { - if (!o.getClass().isInstance(value) && !(o instanceof Number && value instanceof Number)) { - return null; - } +package com.optimizely.ab.config.audience.match; - T rv = (T) o; +/** + * UnknownValueTypeException is thrown when the passed in value for a user attribute does + * not map to a known allowable type. + */ +public class UnknownValueTypeException extends Exception { + private static String message = "has an unsupported attribute value."; - return rv; - } catch (Exception e) { - return null; - } + public UnknownValueTypeException() { + super(message); } } diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java index 645025476..772d22ef7 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java @@ -55,11 +55,11 @@ public class AudienceConditionEvaluationTest { @Before public void initialize() { - testUserAttributes = new HashMap(); + testUserAttributes = new HashMap<>(); testUserAttributes.put("browser_type", "chrome"); testUserAttributes.put("device_type", "Android"); - testTypedUserAttributes = new HashMap(); + testTypedUserAttributes = new HashMap<>(); testTypedUserAttributes.put("is_firefox", true); testTypedUserAttributes.put("num_counts", 3.55); testTypedUserAttributes.put("num_size", 3); diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/match/ExactMatchTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/match/ExactMatchTest.java new file mode 100644 index 000000000..5f2d1d62e --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/match/ExactMatchTest.java @@ -0,0 +1,84 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config.audience.match; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class ExactMatchTest { + + private ExactMatch match; + private static final List INVALIDS = Collections.unmodifiableList(Arrays.asList(new byte[0], new Object(), null)); + + @Before + public void setUp() { + match = new ExactMatch(); + } + + @Test + public void testInvalidConditionValues() { + for (Object invalid : INVALIDS) { + try { + match.eval(invalid, "valid"); + fail("should have raised exception"); + } catch (UnexpectedValueTypeException e) { + //pass + } + } + } + + @Test + public void testMismatchClasses() throws Exception { + assertNull(match.eval(false, "false")); + assertNull(match.eval("false", null)); + } + + @Test + public void testStringMatch() throws Exception { + assertEquals(Boolean.TRUE, match.eval("", "")); + assertEquals(Boolean.TRUE, match.eval("true", "true")); + assertEquals(Boolean.FALSE, match.eval("true", "false")); + } + + @Test + public void testBooleanMatch() throws Exception { + assertEquals(Boolean.TRUE, match.eval(true, true)); + assertEquals(Boolean.TRUE, match.eval(false, false)); + assertEquals(Boolean.FALSE, match.eval(true, false)); + } + + @Test + public void testNumberMatch() throws UnexpectedValueTypeException { + assertEquals(Boolean.TRUE, match.eval(1, 1)); + assertEquals(Boolean.TRUE, match.eval(1L, 1L)); + assertEquals(Boolean.TRUE, match.eval(1.0, 1.0)); + assertEquals(Boolean.TRUE, match.eval(1, 1.0)); + assertEquals(Boolean.TRUE, match.eval(1L, 1.0)); + + assertEquals(Boolean.FALSE, match.eval(1, 2)); + assertEquals(Boolean.FALSE, match.eval(1L, 2L)); + assertEquals(Boolean.FALSE, match.eval(1.0, 2.0)); + assertEquals(Boolean.FALSE, match.eval(1, 1.1)); + assertEquals(Boolean.FALSE, match.eval(1L, 1.1)); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/match/MatchRegistryTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/match/MatchRegistryTest.java new file mode 100644 index 000000000..cb6f2059e --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/match/MatchRegistryTest.java @@ -0,0 +1,61 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config.audience.match; + +import org.junit.Test; + +import static com.optimizely.ab.config.audience.match.MatchRegistry.*; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.*; + +public class MatchRegistryTest { + + @Test + public void testDefaultMatchers() throws UnknownMatchTypeException { + assertThat(MatchRegistry.getMatch(EXACT), instanceOf(ExactMatch.class)); + assertThat(MatchRegistry.getMatch(EXISTS), instanceOf(ExistsMatch.class)); + assertThat(MatchRegistry.getMatch(GREATER_THAN), instanceOf(GTMatch.class)); + assertThat(MatchRegistry.getMatch(LESS_THAN), instanceOf(LTMatch.class)); + assertThat(MatchRegistry.getMatch(GREATER_THAN_EQ), instanceOf(GEMatch.class)); + assertThat(MatchRegistry.getMatch(LESS_THAN_EQ), instanceOf(LEMatch.class)); + assertThat(MatchRegistry.getMatch(LEGACY), instanceOf(DefaultMatchForLegacyAttributes.class)); + assertThat(MatchRegistry.getMatch(SEMVER_EQ), instanceOf(SemanticVersionEqualsMatch.class)); + assertThat(MatchRegistry.getMatch(SEMVER_GE), instanceOf(SemanticVersionGEMatch.class)); + assertThat(MatchRegistry.getMatch(SEMVER_GT), instanceOf(SemanticVersionGTMatch.class)); + assertThat(MatchRegistry.getMatch(SEMVER_LE), instanceOf(SemanticVersionLEMatch.class)); + assertThat(MatchRegistry.getMatch(SEMVER_LT), instanceOf(SemanticVersionLTMatch.class)); + assertThat(MatchRegistry.getMatch(SUBSTRING), instanceOf(SubstringMatch.class)); + } + + @Test(expected = UnknownMatchTypeException.class) + public void testUnknownMatcher() throws UnknownMatchTypeException { + MatchRegistry.getMatch("UNKNOWN"); + } + + @Test + public void testRegister() throws UnknownMatchTypeException { + class TestMatcher implements Match { + @Override + public Boolean eval(Object conditionValue, Object attributeValue) { + return null; + } + } + + MatchRegistry.register("test-matcher", new TestMatcher()); + assertThat(MatchRegistry.getMatch("test-matcher"), instanceOf(TestMatcher.class)); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/match/NumberComparatorTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/match/NumberComparatorTest.java new file mode 100644 index 000000000..19d67dd33 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/match/NumberComparatorTest.java @@ -0,0 +1,75 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config.audience.match; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class NumberComparatorTest { + + private static final List INVALIDS = Collections.unmodifiableList(Arrays.asList(null, "test", "", true)); + + @Test + public void testLessThan() throws UnknownValueTypeException { + assertTrue(NumberComparator.compare(0,1) < 0); + assertTrue(NumberComparator.compare(0,1.0) < 0); + assertTrue(NumberComparator.compare(0,1L) < 0); + } + + @Test + public void testGreaterThan() throws UnknownValueTypeException { + assertTrue(NumberComparator.compare(1,0) > 0); + assertTrue(NumberComparator.compare(1.0,0) > 0); + assertTrue(NumberComparator.compare(1L,0) > 0); + } + + @Test + public void testEquals() throws UnknownValueTypeException { + assertEquals(0, NumberComparator.compare(1, 1)); + assertEquals(0, NumberComparator.compare(1, 1.0)); + assertEquals(0, NumberComparator.compare(1L, 1)); + } + + @Test + public void testInvalidRight() { + for (Object invalid: INVALIDS) { + try { + NumberComparator.compare(0, invalid); + fail("should have failed for invalid object"); + } catch (UnknownValueTypeException e) { + // pass + } + } + } + + @Test + public void testInvalidLeft() { + for (Object invalid: INVALIDS) { + try { + NumberComparator.compare(invalid, 0); + fail("should have failed for invalid object"); + } catch (UnknownValueTypeException e) { + // pass + } + } + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/SemanticVersionTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/match/SemanticVersionTest.java similarity index 52% rename from core-api/src/test/java/com/optimizely/ab/config/audience/SemanticVersionTest.java rename to core-api/src/test/java/com/optimizely/ab/config/audience/match/SemanticVersionTest.java index 6d4605e54..1b819d418 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/audience/SemanticVersionTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/match/SemanticVersionTest.java @@ -14,9 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.optimizely.ab.config.audience; +package com.optimizely.ab.config.audience.match; -import com.optimizely.ab.config.audience.match.SemanticVersion; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -141,108 +140,29 @@ public void semanticVersionInvalidShouldBeOfSizeLessThan3() throws Exception { } @Test - public void semanticVersionCompareTo() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1"); - SemanticVersion actualSV = new SemanticVersion("3.7.1"); - assertTrue(actualSV.compare(targetSV) == 0); + public void testEquals() throws Exception { + assertEquals(0, SemanticVersion.compare("3.7.1", "3.7.1")); + assertEquals(0, SemanticVersion.compare("3.7.1", "3.7")); + assertEquals(0, SemanticVersion.compare("2.1.3+build", "2.1.3")); + assertEquals(0, SemanticVersion.compare("3.7.1-beta.1+2.3", "3.7.1-beta.1+2.3")); } @Test - public void semanticVersionCompareToActualLess() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1"); - SemanticVersion actualSV = new SemanticVersion("3.7.0"); - assertTrue(actualSV.compare(targetSV) < 0); + public void testLessThan() throws Exception { + assertTrue(SemanticVersion.compare("3.7.0", "3.7.1") < 0); + assertTrue(SemanticVersion.compare("3.7", "3.7.1") < 0); + assertTrue(SemanticVersion.compare("2.1.3-beta+1", "2.1.3-beta+1.2.3") < 0); + assertTrue(SemanticVersion.compare("2.1.3-beta-1", "2.1.3-beta-1.2.3") < 0); } @Test - public void semanticVersionCompareToActualGreater() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1"); - SemanticVersion actualSV = new SemanticVersion("3.7.2"); - assertTrue(actualSV.compare(targetSV) > 0); - } - - @Test - public void semanticVersionCompareToPatchMissing() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7"); - SemanticVersion actualSV = new SemanticVersion("3.7.1"); - assertTrue(actualSV.compare(targetSV) == 0); - } - - @Test - public void semanticVersionCompareToActualPatchMissing() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1"); - SemanticVersion actualSV = new SemanticVersion("3.7"); - assertTrue(actualSV.compare(targetSV) < 0); - } - - @Test - public void semanticVersionCompareToActualPreReleaseMissing() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1-beta"); - SemanticVersion actualSV = new SemanticVersion("3.7.1"); - assertTrue(actualSV.compare(targetSV) > 0); - } - - @Test - public void semanticVersionCompareTargetBetaComplex() throws Exception { - SemanticVersion targetSV = new SemanticVersion("2.1.3-beta+1"); - SemanticVersion actualSV = new SemanticVersion("2.1.3-beta+1.2.3"); - assertTrue(actualSV.compare(targetSV) > 0); - } - - @Test - public void semanticVersionCompareTargetBuildIgnores() throws Exception { - SemanticVersion targetSV = new SemanticVersion("2.1.3"); - SemanticVersion actualSV = new SemanticVersion("2.1.3+build"); - assertTrue(actualSV.compare(targetSV) == 0); - } - - @Test - public void semanticVersionCompareTargetBuildComplex() throws Exception { - SemanticVersion targetSV = new SemanticVersion("2.1.3-beta+1.2.3"); - SemanticVersion actualSV = new SemanticVersion("2.1.3-beta+1"); - assertTrue(actualSV.compare(targetSV) < 0); - } - - @Test - public void semanticVersionCompareMultipleDash() throws Exception { - SemanticVersion targetSV = new SemanticVersion("2.1.3-beta-1.2.3"); - SemanticVersion actualSV = new SemanticVersion("2.1.3-beta-1"); - assertTrue(actualSV.compare(targetSV) < 0); - } - - @Test - public void semanticVersionCompareToAlphaBetaAsciiComparision() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1-alpha"); - SemanticVersion actualSV = new SemanticVersion("3.7.1-beta"); - assertTrue(actualSV.compare(targetSV) > 0); - } - - @Test - public void semanticVersionComparePrereleaseSmallerThanBuild() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1-prerelease"); - SemanticVersion actualSV = new SemanticVersion("3.7.1+build"); - assertTrue(actualSV.compare(targetSV) > 0); - } - - - @Test - public void semanticVersionCompareAgainstPreReleaseToPreRelease() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1-prerelease+build"); - SemanticVersion actualSV = new SemanticVersion("3.7.1-prerelease-prerelease+rc"); - assertTrue(actualSV.compare(targetSV) > 0); - } - - @Test - public void semanticVersionCompareToIgnoreMetaComparision() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1-beta.1+2.3"); - SemanticVersion actualSV = new SemanticVersion("3.7.1-beta.1+2.3"); - assertTrue(actualSV.compare(targetSV) == 0); - } - - @Test - public void semanticVersionCompareToPreReleaseComparision() throws Exception { - SemanticVersion targetSV = new SemanticVersion("3.7.1-beta.1"); - SemanticVersion actualSV = new SemanticVersion("3.7.1-beta.2"); - assertTrue(actualSV.compare(targetSV) > 0); + public void testGreaterThan() throws Exception { + assertTrue(SemanticVersion.compare("3.7.2", "3.7.1") > 0); + assertTrue(SemanticVersion.compare("3.7.1", "3.7.1-beta") > 0); + assertTrue(SemanticVersion.compare("2.1.3-beta+1.2.3", "2.1.3-beta+1") > 0); + assertTrue(SemanticVersion.compare("3.7.1-beta", "3.7.1-alpha") > 0); + assertTrue(SemanticVersion.compare("3.7.1+build", "3.7.1-prerelease") > 0); + assertTrue(SemanticVersion.compare("3.7.1-prerelease-prerelease+rc", "3.7.1-prerelease+build") > 0); + assertTrue(SemanticVersion.compare("3.7.1-beta.2", "3.7.1-beta.1") > 0); } } diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/match/SubstringMatchTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/match/SubstringMatchTest.java new file mode 100644 index 000000000..0d417eefe --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/match/SubstringMatchTest.java @@ -0,0 +1,65 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config.audience.match; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class SubstringMatchTest { + + private SubstringMatch match; + private static final List INVALIDS = Collections.unmodifiableList(Arrays.asList(new byte[0], new Object(), null)); + + @Before + public void setUp() { + match = new SubstringMatch(); + } + + @Test + public void testInvalidConditionValues() { + for (Object invalid : INVALIDS) { + try { + match.eval(invalid, "valid"); + fail("should have raised exception"); + } catch (UnexpectedValueTypeException e) { + //pass + } + } + } + + @Test + public void testInvalidAttributesValues() throws UnexpectedValueTypeException { + for (Object invalid : INVALIDS) { + assertNull(match.eval("valid", invalid)); + } + } + + @Test + public void testStringMatch() throws Exception { + assertEquals(Boolean.TRUE, match.eval("", "any")); + assertEquals(Boolean.TRUE, match.eval("same", "same")); + assertEquals(Boolean.TRUE, match.eval("a", "ab")); + assertEquals(Boolean.FALSE, match.eval("ab", "a")); + assertEquals(Boolean.FALSE, match.eval("a", "b")); + } +}