From a746e1bb1d2f8e0003e49cd8eb9936785897b5ea Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Fri, 8 Sep 2023 13:06:38 -0700 Subject: [PATCH 01/13] fix resource loading Signed-off-by: Kavindu Dodanduwa --- .../resolver/process/model/FlagParser.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index a0afdc760..f89f42ca5 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -10,11 +10,10 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.extern.java.Log; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.net.URL; +import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -42,15 +41,18 @@ public class FlagParser { private static JsonSchema SCHEMA_VALIDATOR; static { - try { - final URL url = FlagParser.class.getClassLoader().getResource(SCHEMA_RESOURCE); - if (url == null) { + try( final InputStream schema = FlagParser.class.getClassLoader().getResourceAsStream(SCHEMA_RESOURCE)) { + if (schema == null) { log.log(Level.WARNING, String.format("Resource %s not found", SCHEMA_RESOURCE)); } else { - byte[] bytes = Files.readAllBytes(Paths.get(url.getPath())); - String schemaString = new String(bytes, StandardCharsets.UTF_8); + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[512]; + for (int size; 0 <(size = schema.read(buffer));){ + result.write(buffer, 0, size); + } + JsonSchemaFactory instance = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); - SCHEMA_VALIDATOR = instance.getSchema(schemaString); + SCHEMA_VALIDATOR = instance.getSchema(result.toString()); } } catch (Throwable e) { // log only, do not throw From ef30c626e5c9811c5e4338e626abd5d681004174 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Fri, 8 Sep 2023 14:41:18 -0700 Subject: [PATCH 02/13] evaluators Signed-off-by: Kavindu Dodanduwa --- .../resolver/process/InProcessResolver.java | 8 +++ .../resolver/process/model/FlagParser.java | 1 - .../resolver/process/operator/Fractional.java | 20 ++++++ .../resolver/process/operator/SemVer.java | 23 +++++++ .../resolver/process/operator/StringComp.java | 65 +++++++++++++++++++ 5 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/Fractional.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/SemVer.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/StringComp.java diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index fb4c42cc4..a682647c7 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -3,6 +3,9 @@ import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import dev.openfeature.contrib.providers.flagd.resolver.process.operator.Fractional; +import dev.openfeature.contrib.providers.flagd.resolver.process.operator.SemVer; +import dev.openfeature.contrib.providers.flagd.resolver.process.operator.StringComp; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; @@ -39,7 +42,12 @@ public InProcessResolver(FlagdOptions options, Consumer stateCons // currently we support gRPC connector this.flagStore = new FlagStore(new GrpcStreamConnector(options)); this.stateConsumer = stateConsumer; + jsonLogicHandler = new JsonLogic(); + jsonLogicHandler.addOperation(new Fractional()); + jsonLogicHandler.addOperation(new SemVer()); + jsonLogicHandler.addOperation(new StringComp(StringComp.Type.STARTS_WITH)); + jsonLogicHandler.addOperation(new StringComp(StringComp.Type.ENDS_WITH)); } /** diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index f89f42ca5..b63e8fba9 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -13,7 +13,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Iterator; import java.util.Map; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/Fractional.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/Fractional.java new file mode 100644 index 000000000..dcda17a65 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/Fractional.java @@ -0,0 +1,20 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.operator; + +import io.github.jamsesso.jsonlogic.ast.JsonLogicArray; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluator; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicExpression; + +public class Fractional implements JsonLogicExpression { + + public String key() { + return "fractionalEvaluation"; + } + + public Object evaluate(JsonLogicEvaluator evaluator, JsonLogicArray arguments, Object data) + throws JsonLogicEvaluationException { + + // todo implement + return null; + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/SemVer.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/SemVer.java new file mode 100644 index 000000000..e0bcdbb0e --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/SemVer.java @@ -0,0 +1,23 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.operator; + +import io.github.jamsesso.jsonlogic.ast.JsonLogicArray; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluator; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicExpression; + +public class SemVer implements JsonLogicExpression { + + // todo find sem ver impl library + + + public String key() { + return "sem_ver"; + } + + public Object evaluate(JsonLogicEvaluator evaluator, JsonLogicArray arguments, Object data) + throws JsonLogicEvaluationException { + + // todo implement + return null; + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/StringComp.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/StringComp.java new file mode 100644 index 000000000..1371ab1bf --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/StringComp.java @@ -0,0 +1,65 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.operator; + +import io.github.jamsesso.jsonlogic.ast.JsonLogicArray; +import io.github.jamsesso.jsonlogic.ast.JsonLogicNode; +import io.github.jamsesso.jsonlogic.ast.JsonLogicString; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluator; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicExpression; + +public class StringComp implements JsonLogicExpression { + private final Type type; + + public StringComp(Type type){ + this.type = type; + } + + public String key() { + return type.key; + } + + public Object evaluate(JsonLogicEvaluator evaluator, JsonLogicArray arguments, Object data) + throws JsonLogicEvaluationException { + if (arguments.size() != 2){ + return null; + } + + JsonLogicNode jsonLogicNode = arguments.get(0); + + if (!(jsonLogicNode instanceof JsonLogicString)){ + return null; + } + + final String arg1 = ((JsonLogicString) jsonLogicNode).getValue(); + + jsonLogicNode = arguments.get(1); + + if (!(jsonLogicNode instanceof JsonLogicString)){ + return null; + } + + final String arg2 = ((JsonLogicString) jsonLogicNode).getValue(); + + switch (this.type){ + case STARTS_WITH: + return arg1.startsWith(arg2); + case ENDS_WITH: + return arg1.endsWith(arg2); + default: + throw new JsonLogicEvaluationException(String.format("Unknown string comparison evaluator type %s", + this.type)); + } + } + + + public enum Type{ + STARTS_WITH("starts_with"), + ENDS_WITH("ends_with"); + + private final String key; + + Type(String key) { + this.key = key; + } + } +} From ff14ff27953bd5deda32a6a02991bcc96bfa7321 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Tue, 12 Sep 2023 14:07:41 -0700 Subject: [PATCH 03/13] fractional eval and others Signed-off-by: Kavindu Dodanduwa --- providers/flagd/pom.xml | 7 + .../resolver/process/InProcessResolver.java | 20 +- .../resolver/process/operator/Fractional.java | 20 - .../resolver/process/operator/SemVer.java | 23 - .../resolver/process/operator/StringComp.java | 65 -- .../process/targeting/Fractional.java | 124 ++++ .../resolver/process/targeting/Operator.java | 74 +++ .../resolver/process/targeting/SemVer.java | 113 ++++ .../process/targeting/StringComp.java | 68 +++ .../targeting/TargetingRuleException.java | 8 + .../resolver/process/targeting/flag.json | 14 + .../process/targeting/lib/Murmur3.java | 557 ++++++++++++++++++ .../process/targeting/FractionalTest.java | 100 ++++ .../process/targeting/SemVerTest.java | 186 ++++++ .../process/targeting/StringCompTest.java | 82 +++ 15 files changed, 1339 insertions(+), 122 deletions(-) delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/Fractional.java delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/SemVer.java delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/StringComp.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/StringComp.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/TargetingRuleException.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/flag.json create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVerTest.java create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/StringCompTest.java diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml index 57a6758ac..cc83f91d1 100644 --- a/providers/flagd/pom.xml +++ b/providers/flagd/pom.xml @@ -114,6 +114,13 @@ opentelemetry-api 1.30.1 + + + org.semver4j + semver4j + 5.1.0 + + diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index a682647c7..339adbd2a 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -3,21 +3,18 @@ import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; -import dev.openfeature.contrib.providers.flagd.resolver.process.operator.Fractional; -import dev.openfeature.contrib.providers.flagd.resolver.process.operator.SemVer; -import dev.openfeature.contrib.providers.flagd.resolver.process.operator.StringComp; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.grpc.GrpcStreamConnector; +import dev.openfeature.contrib.providers.flagd.resolver.process.targeting.Operator; +import dev.openfeature.contrib.providers.flagd.resolver.process.targeting.TargetingRuleException; import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Reason; import dev.openfeature.sdk.Value; -import io.github.jamsesso.jsonlogic.JsonLogic; -import io.github.jamsesso.jsonlogic.JsonLogicException; import lombok.extern.java.Log; import java.util.function.Consumer; @@ -33,7 +30,7 @@ public class InProcessResolver implements Resolver { private final Storage flagStore; private final Consumer stateConsumer; - private final JsonLogic jsonLogicHandler; + private final Operator operator; /** * Initialize an in-process resolver. @@ -42,12 +39,7 @@ public InProcessResolver(FlagdOptions options, Consumer stateCons // currently we support gRPC connector this.flagStore = new FlagStore(new GrpcStreamConnector(options)); this.stateConsumer = stateConsumer; - - jsonLogicHandler = new JsonLogic(); - jsonLogicHandler.addOperation(new Fractional()); - jsonLogicHandler.addOperation(new SemVer()); - jsonLogicHandler.addOperation(new StringComp(StringComp.Type.STARTS_WITH)); - jsonLogicHandler.addOperation(new StringComp(StringComp.Type.ENDS_WITH)); + this.operator = new Operator(); } /** @@ -168,7 +160,7 @@ private ProviderEvaluation resolve(Class type, String key, T defaultVa reason = Reason.STATIC.toString(); } else { try { - final Object jsonResolved = jsonLogicHandler.apply(flag.getTargeting(), ctx.asObjectMap()); + final Object jsonResolved = operator.apply(key, flag.getTargeting(), ctx); if (jsonResolved == null) { resolvedVariant = flag.getDefaultVariant(); reason = Reason.DEFAULT.toString(); @@ -176,7 +168,7 @@ private ProviderEvaluation resolve(Class type, String key, T defaultVa resolvedVariant = jsonResolved; reason = Reason.TARGETING_MATCH.toString(); } - } catch (JsonLogicException e) { + } catch (TargetingRuleException e) { log.log(Level.FINE, "Error evaluating targeting rule", e); return ProviderEvaluation.builder() .value(defaultValue) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/Fractional.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/Fractional.java deleted file mode 100644 index dcda17a65..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/Fractional.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.operator; - -import io.github.jamsesso.jsonlogic.ast.JsonLogicArray; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluator; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicExpression; - -public class Fractional implements JsonLogicExpression { - - public String key() { - return "fractionalEvaluation"; - } - - public Object evaluate(JsonLogicEvaluator evaluator, JsonLogicArray arguments, Object data) - throws JsonLogicEvaluationException { - - // todo implement - return null; - } -} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/SemVer.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/SemVer.java deleted file mode 100644 index e0bcdbb0e..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/SemVer.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.operator; - -import io.github.jamsesso.jsonlogic.ast.JsonLogicArray; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluator; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicExpression; - -public class SemVer implements JsonLogicExpression { - - // todo find sem ver impl library - - - public String key() { - return "sem_ver"; - } - - public Object evaluate(JsonLogicEvaluator evaluator, JsonLogicArray arguments, Object data) - throws JsonLogicEvaluationException { - - // todo implement - return null; - } -} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/StringComp.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/StringComp.java deleted file mode 100644 index 1371ab1bf..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/operator/StringComp.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.openfeature.contrib.providers.flagd.resolver.process.operator; - -import io.github.jamsesso.jsonlogic.ast.JsonLogicArray; -import io.github.jamsesso.jsonlogic.ast.JsonLogicNode; -import io.github.jamsesso.jsonlogic.ast.JsonLogicString; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluator; -import io.github.jamsesso.jsonlogic.evaluator.JsonLogicExpression; - -public class StringComp implements JsonLogicExpression { - private final Type type; - - public StringComp(Type type){ - this.type = type; - } - - public String key() { - return type.key; - } - - public Object evaluate(JsonLogicEvaluator evaluator, JsonLogicArray arguments, Object data) - throws JsonLogicEvaluationException { - if (arguments.size() != 2){ - return null; - } - - JsonLogicNode jsonLogicNode = arguments.get(0); - - if (!(jsonLogicNode instanceof JsonLogicString)){ - return null; - } - - final String arg1 = ((JsonLogicString) jsonLogicNode).getValue(); - - jsonLogicNode = arguments.get(1); - - if (!(jsonLogicNode instanceof JsonLogicString)){ - return null; - } - - final String arg2 = ((JsonLogicString) jsonLogicNode).getValue(); - - switch (this.type){ - case STARTS_WITH: - return arg1.startsWith(arg2); - case ENDS_WITH: - return arg1.endsWith(arg2); - default: - throw new JsonLogicEvaluationException(String.format("Unknown string comparison evaluator type %s", - this.type)); - } - } - - - public enum Type{ - STARTS_WITH("starts_with"), - ENDS_WITH("ends_with"); - - private final String key; - - Type(String key) { - this.key = key; - } - } -} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java new file mode 100644 index 000000000..e51aae71f --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java @@ -0,0 +1,124 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +import dev.openfeature.contrib.providers.flagd.resolver.process.targeting.lib.Murmur3; +import io.github.jamsesso.jsonlogic.JsonLogicException; +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import io.github.jamsesso.jsonlogic.evaluator.expressions.PreEvaluatedArgumentsExpression; +import lombok.Getter; +import lombok.extern.java.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; + +@Log +class Fractional implements PreEvaluatedArgumentsExpression { + + public String key() { + return "fractional"; + } + + public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationException { + if (arguments.size() < 2) { + return null; + } + + final Operator.FlagProperties properties = new Operator.FlagProperties(data); + + // check optional string target in first arg + Object arg1 = arguments.get(0); + + final String bucketBy; + final Object[] distibutions; + + if (arg1 instanceof String) { + // first arg is a String, use for bucketing + bucketBy = (String) arg1; + + Object[] source = arguments.toArray(); + distibutions = Arrays.copyOfRange(source, 1, source.length); + } else { + // fallback to targeting key if present + if (properties.getTargetingKey() == null) { + log.log(Level.FINE, "Missing fallback targeting key"); + return null; + } + + bucketBy = properties.getTargetingKey(); + distibutions = arguments.toArray(); + } + + final String hashKey = properties.getFlagKey() + bucketBy; + final List propertyList = new ArrayList<>(); + + double distribution = 0; + try { + for (Object dist : distibutions) { + FractionProperty fractionProperty = new FractionProperty(dist); + propertyList.add(fractionProperty); + distribution += fractionProperty.getPercentage(); + } + } catch (JsonLogicException e) { + log.log(Level.FINE, "Error parsing fractional targeting rule", e); + return null; + } + + if (distribution != 100) { + log.log(Level.FINE, "Fractional properties do not sum to 100"); + return null; + } + + // find distribution + return distributeValue(hashKey, propertyList); + } + + private static String distributeValue(final String hashKey, final List propertyList) + throws JsonLogicEvaluationException { + double bucket = (Math.abs(Murmur3.hash64(hashKey.getBytes())) * 1.0d / Long.MAX_VALUE) * 100.0d; + + double bucketSum = 0; + for (FractionProperty p : propertyList) { + bucketSum += p.getPercentage(); + + if (bucket < bucketSum) { + return p.getVariant(); + } + } + + // this shall not be reached + throw new JsonLogicEvaluationException("Unable to find a correct bucket"); + } + + @Getter + private static class FractionProperty { + private final String variant; + private final double percentage; + + FractionProperty(final Object from) throws JsonLogicException { + if (!(from instanceof List)) { + throw new JsonLogicException("Property is not an array"); + } + + final List array = (List) from; + + if (array.size() != 2) { + throw new JsonLogicException("Fraction property does not have two elements"); + } + + // first must be a string + if (!(array.get(0) instanceof String)) { + throw new JsonLogicException("First element of the fraction property is not a string variant"); + } + + // second element must be a number + if (!(array.get(1) instanceof Number)) { + throw new JsonLogicException("Second element of the fraction property is not a number"); + } + + variant = ((String) array.get(0)); + percentage = ((Number) array.get(1)).doubleValue(); + } + + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java new file mode 100644 index 000000000..d64df41f1 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java @@ -0,0 +1,74 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +import dev.openfeature.sdk.EvaluationContext; +import io.github.jamsesso.jsonlogic.JsonLogic; +import io.github.jamsesso.jsonlogic.JsonLogicException; +import lombok.Getter; + +import java.util.Map; + +/** + * Targeting operator wraps JsonLogic handlers and expose a simple API for external layers. + * This helps to isolate external dependencies to this package. + */ +public class Operator { + + final static String FLAG_KEY = "$flagKey"; + final static String TARGET_KEY = "targetingKey"; + + private final JsonLogic jsonLogicHandler; + + /** + * Construct a targeting operator. + */ + public Operator() { + jsonLogicHandler = new JsonLogic(); + jsonLogicHandler.addOperation(new Fractional()); + jsonLogicHandler.addOperation(new SemVer()); + jsonLogicHandler.addOperation(new StringComp(StringComp.Type.STARTS_WITH)); + jsonLogicHandler.addOperation(new StringComp(StringComp.Type.ENDS_WITH)); + } + + public Object apply(final String flagKey, final String targetingRule, final EvaluationContext ctx) + throws TargetingRuleException { + final Map valueMap = ctx.asObjectMap(); + valueMap.put(FLAG_KEY, flagKey); + + try { + return jsonLogicHandler.apply(targetingRule, valueMap); + } catch (JsonLogicException e) { + throw new TargetingRuleException("Error evaluating json logic", e); + } + } + + @Getter + static class FlagProperties { + private final String flagKey; + private final String targetingKey; + + FlagProperties(Object from) { + if (from instanceof Map) { + Map dataMap = (Map) from; + + Object fKey = dataMap.get(FLAG_KEY); + + if (fKey instanceof String) { + flagKey = (String) fKey; + } else { + flagKey = null; + } + + Object tKey = dataMap.get(TARGET_KEY); + + if (tKey instanceof String) { + targetingKey = (String) tKey; + } else { + targetingKey = null; + } + } else { + flagKey = null; + targetingKey = null; + } + } + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java new file mode 100644 index 000000000..4ab9a9e6a --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java @@ -0,0 +1,113 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import io.github.jamsesso.jsonlogic.evaluator.expressions.PreEvaluatedArgumentsExpression; +import lombok.extern.java.Log; +import org.semver4j.Semver; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; + +@Log +class SemVer implements PreEvaluatedArgumentsExpression { + + private static final String EQ = "="; + private static final String NEQ = "!="; + private static final String LT = "<"; + private static final String LTE = "<="; + private static final String GT = ">"; + private static final String GTE = ">="; + private static final String MAJOR = "^"; + private static final String MINOR = "~"; + + private static final Set OPS = new HashSet<>(); + + static { + OPS.add(EQ); + OPS.add(NEQ); + OPS.add(LT); + OPS.add(LTE); + OPS.add(GT); + OPS.add(GTE); + OPS.add(MAJOR); + OPS.add(MINOR); + } + + public String key() { + return "sem_ver"; + } + + public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationException { + + if (arguments.size() != 3) { + log.log(Level.FINE, "Incorrect number of arguments for sem_ver operator"); + return null; + } + + for (int i = 0; i < 3; i++) { + if (arguments.get(i) instanceof String) { + continue; + } + + log.log(Level.FINE, "Invalid argument type. Require Strings"); + return null; + } + + // arg 1 should be a SemVer + final Semver arg1Parsed; + + if ((arg1Parsed = Semver.parse((String) arguments.get(0))) == null) { + log.log(Level.FINE, "Argument one is not a valid SemVer"); + return null; + } + + // arg 2 should be the supported operator + final String arg2Parsed = (String) arguments.get(1); + + if (!OPS.contains(arg2Parsed)) { + log.log(Level.FINE, String.format("Not valid operator in argument 2. Received: %a", arg2Parsed)); + return null; + } + + // arg 3 should be a SemVer + final Semver arg3Parsed; + + if ((arg3Parsed = Semver.parse((String) arguments.get(2))) == null) { + log.log(Level.FINE, "Argument three is not a valid SemVer"); + return null; + } + + return compare(arg2Parsed, arg1Parsed, arg3Parsed); + } + + private static boolean compare(final String operator, final Semver arg1, final Semver arg2) + throws JsonLogicEvaluationException { + + int comp = arg1.compareTo(arg2); + + switch (operator) { + case EQ: + return comp == 0; + case NEQ: + return comp != 0; + case LT: + return comp < 0; + case LTE: + return comp <= 0; + case GT: + return comp > 0; + case GTE: + return comp >= 0; + case MAJOR: + return arg1.getMajor() == arg2.getMajor(); + case MINOR: + return arg1.getMinor() == arg2.getMinor() && arg1.getMajor() == arg2.getMajor(); + default: + throw new JsonLogicEvaluationException( + String.format("Unsupported operator received. Operator: %s", operator)); + } + } + +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/StringComp.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/StringComp.java new file mode 100644 index 000000000..b55d2a4f8 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/StringComp.java @@ -0,0 +1,68 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import io.github.jamsesso.jsonlogic.evaluator.expressions.PreEvaluatedArgumentsExpression; +import lombok.extern.java.Log; + +import java.util.List; +import java.util.logging.Level; + +@Log +class StringComp implements PreEvaluatedArgumentsExpression { + private final Type type; + + StringComp(Type type) { + this.type = type; + } + + public String key() { + return type.key; + } + + public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationException { + if (arguments.size() != 2) { + log.log(Level.FINE, "Incorrect number of arguments for String comparison operator"); + return null; + } + + Object jsonLogicNode = arguments.get(0); + + if (!(jsonLogicNode instanceof String)) { + log.log(Level.FINE, "Incorrect argument type for first argument"); + return null; + } + + final String arg1 = (String) jsonLogicNode; + + jsonLogicNode = arguments.get(1); + + if (!(jsonLogicNode instanceof String)) { + log.log(Level.FINE, "Incorrect argument type for second argument"); + return null; + } + + final String arg2 = (String) jsonLogicNode; + + switch (this.type) { + case STARTS_WITH: + return arg1.startsWith(arg2); + case ENDS_WITH: + return arg1.endsWith(arg2); + default: + log.log(Level.FINE, String.format("Unknown string comparison evaluator type %s", this.type)); + return null; + } + } + + + enum Type { + STARTS_WITH("starts_with"), + ENDS_WITH("ends_with"); + + private final String key; + + Type(String key) { + this.key = key; + } + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/TargetingRuleException.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/TargetingRuleException.java new file mode 100644 index 000000000..8b7f876bc --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/TargetingRuleException.java @@ -0,0 +1,8 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +public class TargetingRuleException extends Exception{ + + public TargetingRuleException(final String message, final Throwable t){ + super(message, t); + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/flag.json b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/flag.json new file mode 100644 index 000000000..079bb2c71 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/flag.json @@ -0,0 +1,14 @@ +{ + "targeting": + [ + "bucketKet", + [ + "red", + 50 + ], + [ + "blue", + 50 + ] + ] +} \ No newline at end of file diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java new file mode 100644 index 000000000..b383574d0 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java @@ -0,0 +1,557 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Murmur3 is successor to Murmur2 fast non-crytographic hash algorithms. + * + * Murmur3 32 and 128 bit variants. + * 32-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp#94 + * 128-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp#255 + * + * This is a public domain code with no copyrights. + * From homepage of MurmurHash (https://code.google.com/p/smhasher/), + * "All MurmurHash versions are public domain software, and the author disclaims all copyright + * to their code." + * + * This code is burrowed from Apache Hive (https://github.com/apache/hive) + * + * @see + * Apache Hive Murmer3 + */ + +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting.lib; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class Murmur3 { + // from 64-bit linear congruential generator + public static final long NULL_HASHCODE = 2862933555777941757L; + + // Constants for 32 bit variant + private static final int C1_32 = 0xcc9e2d51; + private static final int C2_32 = 0x1b873593; + private static final int R1_32 = 15; + private static final int R2_32 = 13; + private static final int M_32 = 5; + private static final int N_32 = 0xe6546b64; + + // Constants for 128 bit variant + private static final long C1 = 0x87c37b91114253d5L; + private static final long C2 = 0x4cf5ad432745937fL; + private static final int R1 = 31; + private static final int R2 = 27; + private static final int R3 = 33; + private static final int M = 5; + private static final int N1 = 0x52dce729; + private static final int N2 = 0x38495ab5; + + public static final int DEFAULT_SEED = 104729; + + public static int hash32(long l0, long l1) { + return hash32(l0, l1, DEFAULT_SEED); + } + + public static int hash32(long l0) { + return hash32(l0, DEFAULT_SEED); + } + + /** + * Murmur3 32-bit variant. + */ + public static int hash32(long l0, int seed) { + int hash = seed; + final long r0 = Long.reverseBytes(l0); + + hash = mix32((int) r0, hash); + hash = mix32((int) (r0 >>> 32), hash); + + return fmix32(Long.BYTES, hash); + } + + /** + * Murmur3 32-bit variant. + */ + public static int hash32(long l0, long l1, int seed) { + int hash = seed; + final long r0 = Long.reverseBytes(l0); + final long r1 = Long.reverseBytes(l1); + + hash = mix32((int) r0, hash); + hash = mix32((int) (r0 >>> 32), hash); + hash = mix32((int) (r1), hash); + hash = mix32((int) (r1 >>> 32), hash); + + return fmix32(Long.BYTES * 2, hash); + } + + /** + * Murmur3 32-bit variant. + * + * @param data - input byte array + * @return - hashcode + */ + public static int hash32(byte[] data) { + return hash32(data, 0, data.length, DEFAULT_SEED); + } + + /** + * Murmur3 32-bit variant. + * + * @param data - input byte array + * @param length - length of array + * @return - hashcode + */ + public static int hash32(byte[] data, int length) { + return hash32(data, 0, length, DEFAULT_SEED); + } + + /** + * Murmur3 32-bit variant. + * + * @param data - input byte array + * @param length - length of array + * @param seed - seed. (default 0) + * @return - hashcode + */ + public static int hash32(byte[] data, int length, int seed) { + return hash32(data, 0, length, seed); + } + + /** + * Murmur3 32-bit variant. + * + * @param data - input byte array + * @param offset - offset of data + * @param length - length of array + * @param seed - seed. (default 0) + * @return - hashcode + */ + @SuppressFBWarnings(value = {"SF_SWITCH_FALLTHROUGH", "SF_SWITCH_NO_DEFAULT"}, justification = "Expected") + public static int hash32(byte[] data, int offset, int length, int seed) { + int hash = seed; + final int nblocks = length >> 2; + + // body + for (int i = 0; i < nblocks; i++) { + int i_4 = i << 2; + int k = (data[offset + i_4] & 0xff) + | ((data[offset + i_4 + 1] & 0xff) << 8) + | ((data[offset + i_4 + 2] & 0xff) << 16) + | ((data[offset + i_4 + 3] & 0xff) << 24); + + hash = mix32(k, hash); + } + + // tail + int idx = nblocks << 2; + int k1 = 0; + switch (length - idx) { + case 3: + k1 ^= data[offset + idx + 2] << 16; + case 2: + k1 ^= data[offset + idx + 1] << 8; + case 1: + k1 ^= data[offset + idx]; + + // mix functions + k1 *= C1_32; + k1 = Integer.rotateLeft(k1, R1_32); + k1 *= C2_32; + hash ^= k1; + } + + return fmix32(length, hash); + } + + private static int mix32(int k, int hash) { + k *= C1_32; + k = Integer.rotateLeft(k, R1_32); + k *= C2_32; + hash ^= k; + return Integer.rotateLeft(hash, R2_32) * M_32 + N_32; + } + + private static int fmix32(int length, int hash) { + hash ^= length; + hash ^= (hash >>> 16); + hash *= 0x85ebca6b; + hash ^= (hash >>> 13); + hash *= 0xc2b2ae35; + hash ^= (hash >>> 16); + + return hash; + } + + /** + * Murmur3 64-bit variant. This is essentially MSB 8 bytes of Murmur3 128-bit variant. + * + * @param data - input byte array + * @return - hashcode + */ + public static long hash64(byte[] data) { + return hash64(data, 0, data.length, DEFAULT_SEED); + } + + public static long hash64(long data) { + long hash = DEFAULT_SEED; + long k = Long.reverseBytes(data); + int length = Long.BYTES; + // mix functions + k *= C1; + k = Long.rotateLeft(k, R1); + k *= C2; + hash ^= k; + hash = Long.rotateLeft(hash, R2) * M + N1; + // finalization + hash ^= length; + hash = fmix64(hash); + return hash; + } + + public static long hash64(int data) { + long k1 = Integer.reverseBytes(data) & (-1L >>> 32); + int length = Integer.BYTES; + long hash = DEFAULT_SEED; + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + hash ^= k1; + // finalization + hash ^= length; + hash = fmix64(hash); + return hash; + } + + public static long hash64(short data) { + long hash = DEFAULT_SEED; + long k1 = 0; + k1 ^= ((long) data & 0xff) << 8; + k1 ^= ((long)((data & 0xFF00) >> 8) & 0xff); + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + hash ^= k1; + + // finalization + hash ^= Short.BYTES; + hash = fmix64(hash); + return hash; + } + + public static long hash64(byte[] data, int offset, int length) { + return hash64(data, offset, length, DEFAULT_SEED); + } + + /** + * Murmur3 64-bit variant. This is essentially MSB 8 bytes of Murmur3 128-bit variant. + * + * @param data - input byte array + * @param length - length of array + * @param seed - seed. (default is 0) + * @return - hashcode + */ + @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "Expected") + public static long hash64(byte[] data, int offset, int length, int seed) { + long hash = seed; + final int nblocks = length >> 3; + + // body + for (int i = 0; i < nblocks; i++) { + final int i8 = i << 3; + long k = ((long) data[offset + i8] & 0xff) + | (((long) data[offset + i8 + 1] & 0xff) << 8) + | (((long) data[offset + i8 + 2] & 0xff) << 16) + | (((long) data[offset + i8 + 3] & 0xff) << 24) + | (((long) data[offset + i8 + 4] & 0xff) << 32) + | (((long) data[offset + i8 + 5] & 0xff) << 40) + | (((long) data[offset + i8 + 6] & 0xff) << 48) + | (((long) data[offset + i8 + 7] & 0xff) << 56); + + // mix functions + k *= C1; + k = Long.rotateLeft(k, R1); + k *= C2; + hash ^= k; + hash = Long.rotateLeft(hash, R2) * M + N1; + } + + // tail + long k1 = 0; + int tailStart = nblocks << 3; + switch (length - tailStart) { + case 7: + k1 ^= ((long) data[offset + tailStart + 6] & 0xff) << 48; + case 6: + k1 ^= ((long) data[offset + tailStart + 5] & 0xff) << 40; + case 5: + k1 ^= ((long) data[offset + tailStart + 4] & 0xff) << 32; + case 4: + k1 ^= ((long) data[offset + tailStart + 3] & 0xff) << 24; + case 3: + k1 ^= ((long) data[offset + tailStart + 2] & 0xff) << 16; + case 2: + k1 ^= ((long) data[offset + tailStart + 1] & 0xff) << 8; + case 1: + k1 ^= ((long) data[offset + tailStart] & 0xff); + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + hash ^= k1; + } + + // finalization + hash ^= length; + hash = fmix64(hash); + + return hash; + } + + /** + * Murmur3 128-bit variant. + * + * @param data - input byte array + * @return - hashcode (2 longs) + */ + public static long[] hash128(byte[] data) { + return hash128(data, 0, data.length, DEFAULT_SEED); + } + + /** + * Murmur3 128-bit variant. + * + * @param data - input byte array + * @param offset - the first element of array + * @param length - length of array + * @param seed - seed. (default is 0) + * @return - hashcode (2 longs) + */ + @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "Expected") + public static long[] hash128(byte[] data, int offset, int length, int seed) { + long h1 = seed; + long h2 = seed; + final int nblocks = length >> 4; + + // body + for (int i = 0; i < nblocks; i++) { + final int i16 = i << 4; + long k1 = ((long) data[offset + i16] & 0xff) + | (((long) data[offset + i16 + 1] & 0xff) << 8) + | (((long) data[offset + i16 + 2] & 0xff) << 16) + | (((long) data[offset + i16 + 3] & 0xff) << 24) + | (((long) data[offset + i16 + 4] & 0xff) << 32) + | (((long) data[offset + i16 + 5] & 0xff) << 40) + | (((long) data[offset + i16 + 6] & 0xff) << 48) + | (((long) data[offset + i16 + 7] & 0xff) << 56); + + long k2 = ((long) data[offset + i16 + 8] & 0xff) + | (((long) data[offset + i16 + 9] & 0xff) << 8) + | (((long) data[offset + i16 + 10] & 0xff) << 16) + | (((long) data[offset + i16 + 11] & 0xff) << 24) + | (((long) data[offset + i16 + 12] & 0xff) << 32) + | (((long) data[offset + i16 + 13] & 0xff) << 40) + | (((long) data[offset + i16 + 14] & 0xff) << 48) + | (((long) data[offset + i16 + 15] & 0xff) << 56); + + // mix functions for k1 + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + h1 ^= k1; + h1 = Long.rotateLeft(h1, R2); + h1 += h2; + h1 = h1 * M + N1; + + // mix functions for k2 + k2 *= C2; + k2 = Long.rotateLeft(k2, R3); + k2 *= C1; + h2 ^= k2; + h2 = Long.rotateLeft(h2, R1); + h2 += h1; + h2 = h2 * M + N2; + } + + // tail + long k1 = 0; + long k2 = 0; + int tailStart = nblocks << 4; + switch (length - tailStart) { + case 15: + k2 ^= (long) (data[offset + tailStart + 14] & 0xff) << 48; + case 14: + k2 ^= (long) (data[offset + tailStart + 13] & 0xff) << 40; + case 13: + k2 ^= (long) (data[offset + tailStart + 12] & 0xff) << 32; + case 12: + k2 ^= (long) (data[offset + tailStart + 11] & 0xff) << 24; + case 11: + k2 ^= (long) (data[offset + tailStart + 10] & 0xff) << 16; + case 10: + k2 ^= (long) (data[offset + tailStart + 9] & 0xff) << 8; + case 9: + k2 ^= (long) (data[offset + tailStart + 8] & 0xff); + k2 *= C2; + k2 = Long.rotateLeft(k2, R3); + k2 *= C1; + h2 ^= k2; + + case 8: + k1 ^= (long) (data[offset + tailStart + 7] & 0xff) << 56; + case 7: + k1 ^= (long) (data[offset + tailStart + 6] & 0xff) << 48; + case 6: + k1 ^= (long) (data[offset + tailStart + 5] & 0xff) << 40; + case 5: + k1 ^= (long) (data[offset + tailStart + 4] & 0xff) << 32; + case 4: + k1 ^= (long) (data[offset + tailStart + 3] & 0xff) << 24; + case 3: + k1 ^= (long) (data[offset + tailStart + 2] & 0xff) << 16; + case 2: + k1 ^= (long) (data[offset + tailStart + 1] & 0xff) << 8; + case 1: + k1 ^= (long) (data[offset + tailStart] & 0xff); + k1 *= C1; + k1 = Long.rotateLeft(k1, R1); + k1 *= C2; + h1 ^= k1; + } + + // finalization + h1 ^= length; + h2 ^= length; + + h1 += h2; + h2 += h1; + + h1 = fmix64(h1); + h2 = fmix64(h2); + + h1 += h2; + h2 += h1; + + return new long[]{h1, h2}; + } + + private static long fmix64(long h) { + h ^= (h >>> 33); + h *= 0xff51afd7ed558ccdL; + h ^= (h >>> 33); + h *= 0xc4ceb9fe1a85ec53L; + h ^= (h >>> 33); + return h; + } + + public static class IncrementalHash32 { + byte[] tail = new byte[3]; + int tailLen; + int totalLen; + int hash; + + public final void start(int hash) { + tailLen = totalLen = 0; + this.hash = hash; + } + + public final void add(byte[] data, int offset, int length) { + if (length == 0) return; + totalLen += length; + if (tailLen + length < 4) { + System.arraycopy(data, offset, tail, tailLen, length); + tailLen += length; + return; + } + int offset2 = 0; + if (tailLen > 0) { + offset2 = (4 - tailLen); + int k = -1; + switch (tailLen) { + case 1: + k = orBytes(tail[0], data[offset], data[offset + 1], data[offset + 2]); + break; + case 2: + k = orBytes(tail[0], tail[1], data[offset], data[offset + 1]); + break; + case 3: + k = orBytes(tail[0], tail[1], tail[2], data[offset]); + break; + default: throw new AssertionError(tailLen); + } + // mix functions + k *= C1_32; + k = Integer.rotateLeft(k, R1_32); + k *= C2_32; + hash ^= k; + hash = Integer.rotateLeft(hash, R2_32) * M_32 + N_32; + } + int length2 = length - offset2; + offset += offset2; + final int nblocks = length2 >> 2; + + for (int i = 0; i < nblocks; i++) { + int i_4 = (i << 2) + offset; + int k = orBytes(data[i_4], data[i_4 + 1], data[i_4 + 2], data[i_4 + 3]); + + // mix functions + k *= C1_32; + k = Integer.rotateLeft(k, R1_32); + k *= C2_32; + hash ^= k; + hash = Integer.rotateLeft(hash, R2_32) * M_32 + N_32; + } + + int consumed = (nblocks << 2); + tailLen = length2 - consumed; + if (consumed == length2) return; + System.arraycopy(data, offset + consumed, tail, 0, tailLen); + } + + @SuppressFBWarnings(value = {"SF_SWITCH_FALLTHROUGH", "SF_SWITCH_NO_DEFAULT"}, justification = "Expected") + public final int end() { + int k1 = 0; + switch (tailLen) { + case 3: + k1 ^= tail[2] << 16; + case 2: + k1 ^= tail[1] << 8; + case 1: + k1 ^= tail[0]; + + // mix functions + k1 *= C1_32; + k1 = Integer.rotateLeft(k1, R1_32); + k1 *= C2_32; + hash ^= k1; + } + + // finalization + hash ^= totalLen; + hash ^= (hash >>> 16); + hash *= 0x85ebca6b; + hash ^= (hash >>> 13); + hash *= 0xc2b2ae35; + hash ^= (hash >>> 16); + return hash; + } + } + + private static int orBytes(byte b1, byte b2, byte b3, byte b4) { + return (b1 & 0xff) | ((b2 & 0xff) << 8) | ((b3 & 0xff) << 16) | ((b4 & 0xff) << 24); + } +} \ No newline at end of file diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java new file mode 100644 index 000000000..67a4316e3 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java @@ -0,0 +1,100 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static dev.openfeature.contrib.providers.flagd.resolver.process.targeting.Operator.FLAG_KEY; +import static dev.openfeature.contrib.providers.flagd.resolver.process.targeting.Operator.TARGET_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FractionalTest { + + @Test void selfContainedFractional() throws JsonLogicEvaluationException { + // given + Fractional fractional = new Fractional(); + + /* Rule + * [ + * "bucketKey", + * [ + * "red", + * 50 + * ], + * [ + * "blue", + * 50 + * ] + * ] + * */ + + final List rule = new ArrayList<>(); + rule.add("bucketKey"); + + final List bucket1 = new ArrayList<>(); + bucket1.add("red"); + bucket1.add(50); + + final List bucket2 = new ArrayList<>(); + bucket2.add("green"); + bucket2.add(50); + + rule.add(bucket1); + rule.add(bucket2); + + Map data = new HashMap<>(); + data.put("$flagKey", "flagA"); + + // when + Object evaluate = fractional.evaluate(rule, data); + + // then + assertEquals("green", evaluate); + } + + @Test void targetingBackedFractional() throws JsonLogicEvaluationException { + // given + Fractional fractional = new Fractional(); + + /* Rule + * [ + * [ + * "red", + * 50 + * ], + * [ + * "blue", + * 50 + * ] + * ] + * */ + + final List rule = new ArrayList<>(); + + final List bucket1 = new ArrayList<>(); + bucket1.add("red"); + bucket1.add(50); + + final List bucket2 = new ArrayList<>(); + bucket2.add("green"); + bucket2.add(50); + + rule.add(bucket1); + rule.add(bucket2); + + Map data = new HashMap<>(); + data.put(FLAG_KEY, "flagA"); + data.put(TARGET_KEY, "targetingKey"); + + // when + Object evaluate = fractional.evaluate(rule, data); + + // then + assertEquals("red", evaluate); + } + +} \ No newline at end of file diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVerTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVerTest.java new file mode 100644 index 000000000..661f3a4bb --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVerTest.java @@ -0,0 +1,186 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +class SemVerTest { + + @Test + public void eqOperator() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("v1.2.3", "=", "1.2.3"), new Object()); + + // then + if (!(result instanceof Boolean)) { + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void neqOperator() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("v1.2.3", "!=", "1.2.4"), new Object()); + + // then + if (!(result instanceof Boolean)) { + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void ltOperator() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("v1.2.3", "<", "1.2.4"), new Object()); + + // then + if (!(result instanceof Boolean)) { + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void lteOperator() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("v1.2.3", "<=", "1.2.3"), new Object()); + + // then + if (!(result instanceof Boolean)) { + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void gtOperator() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("v1.2.3", ">", "0.2.3"), new Object()); + + // then + if (!(result instanceof Boolean)) { + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void gteOperator() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("v1.2.3", ">=", "v1.2.3"), new Object()); + + // then + if (!(result instanceof Boolean)) { + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void majorCompOperator() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("v1.2.3", "^", "v1.0.0"), new Object()); + + // then + if (!(result instanceof Boolean)) { + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void minorCompOperator() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("v5.0.3", "~", "v5.0.8"), new Object()); + + // then + if (!(result instanceof Boolean)) { + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void invalidType() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("1.2.3", "=", 1.2), new Object()); + + assertThat(result).isNull(); + } + + + @Test + public void invalidArg1() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("1.2", "=", "1.2.3"), new Object()); + + assertThat(result).isNull(); + } + + @Test + public void invalidArg2() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("1.2.3", "*", "1.2.3"), new Object()); + + assertThat(result).isNull(); + } + + @Test + public void invalidArg3() throws JsonLogicEvaluationException { + // given + final SemVer semVer = new SemVer(); + + // when + Object result = semVer.evaluate(Arrays.asList("1.2.3", "=", "1.2"), new Object()); + + assertThat(result).isNull(); + } + +} \ No newline at end of file diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/StringCompTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/StringCompTest.java new file mode 100644 index 000000000..21c638ca0 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/StringCompTest.java @@ -0,0 +1,82 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +class StringCompTest { + + + @Test + public void startsWithEvaluation() throws JsonLogicEvaluationException { + // given + final StringComp startsWith = new StringComp(StringComp.Type.STARTS_WITH); + + // when + Object result = startsWith.evaluate(Arrays.asList("abc@123.com", "abc"), new Object()); + + // then + if (!(result instanceof Boolean)){ + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void endsWithEvaluation() throws JsonLogicEvaluationException { + // given + final StringComp endsWith = new StringComp(StringComp.Type.ENDS_WITH); + + // when + Object result = endsWith.evaluate( Arrays.asList("abc@123.com", "123.com"), new Object()); + + // then + if (!(result instanceof Boolean)){ + fail("Result is not of type Boolean"); + } + + assertThat((Boolean) result).isTrue(); + } + + @Test + public void invalidTypeCheckArg1() throws JsonLogicEvaluationException { + // given + final StringComp operator = new StringComp(StringComp.Type.STARTS_WITH); + + // when + Object result = operator.evaluate(Arrays.asList(1230, "12"), new Object()); + + // then + assertThat(result).isNull(); + } + + @Test + public void invalidTypeCheckArg2() throws JsonLogicEvaluationException { + // given + final StringComp operator = new StringComp(StringComp.Type.STARTS_WITH); + + // when + Object result = operator.evaluate(Arrays.asList("abc@123.com", 123), new Object()); + + // then + assertThat(result).isNull(); + } + + @Test + public void invalidNumberOfArgs() throws JsonLogicEvaluationException { + // given + final StringComp operator = new StringComp(StringComp.Type.STARTS_WITH); + + // when + Object result = operator.evaluate(Arrays.asList("123", "12", "1"), new Object()); + + // then + assertThat(result).isNull(); + } + +} \ No newline at end of file From 0967c40221a05fa0c68002bfb8495524250af628 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Tue, 12 Sep 2023 15:15:03 -0700 Subject: [PATCH 04/13] test improvcements Signed-off-by: Kavindu Dodanduwa --- .../resolver/process/targeting/Fractional.java | 5 ++++- .../resolver/process/targeting/FractionalTest.java | 14 +++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java index e51aae71f..45bd176fe 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.extern.java.Log; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -75,7 +76,9 @@ public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationEx private static String distributeValue(final String hashKey, final List propertyList) throws JsonLogicEvaluationException { - double bucket = (Math.abs(Murmur3.hash64(hashKey.getBytes())) * 1.0d / Long.MAX_VALUE) * 100.0d; + byte[] bytes = hashKey.getBytes(StandardCharsets.UTF_8); + double bucket = + (Math.abs(Murmur3.hash64(bytes, 0, bytes.length, 0)) * 1.0d / Long.MAX_VALUE) * 100.0d; double bucketSum = 0; for (FractionProperty p : propertyList) { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java index 67a4316e3..90daf7c3d 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java @@ -47,7 +47,7 @@ class FractionalTest { rule.add(bucket2); Map data = new HashMap<>(); - data.put("$flagKey", "flagA"); + data.put(FLAG_KEY, "flagA"); // when Object evaluate = fractional.evaluate(rule, data); @@ -63,11 +63,11 @@ class FractionalTest { /* Rule * [ * [ - * "red", + * "blue", * 50 * ], * [ - * "blue", + * "green", * 50 * ] * ] @@ -76,7 +76,7 @@ class FractionalTest { final List rule = new ArrayList<>(); final List bucket1 = new ArrayList<>(); - bucket1.add("red"); + bucket1.add("blue"); bucket1.add(50); final List bucket2 = new ArrayList<>(); @@ -87,14 +87,14 @@ class FractionalTest { rule.add(bucket2); Map data = new HashMap<>(); - data.put(FLAG_KEY, "flagA"); - data.put(TARGET_KEY, "targetingKey"); + data.put(FLAG_KEY, "headerColor"); + data.put(TARGET_KEY, "foo@foo.com"); // when Object evaluate = fractional.evaluate(rule, data); // then - assertEquals("red", evaluate); + assertEquals("blue", evaluate); } } \ No newline at end of file From 9b2e63e742c7b873ea75974562d11001910a8745 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Tue, 12 Sep 2023 15:20:36 -0700 Subject: [PATCH 05/13] more tests Signed-off-by: Kavindu Dodanduwa --- .../process/targeting/FractionalTest.java | 124 +++++++++++++++++- 1 file changed, 122 insertions(+), 2 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java index 90daf7c3d..2ae2dfca7 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java @@ -11,10 +11,12 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.targeting.Operator.FLAG_KEY; import static dev.openfeature.contrib.providers.flagd.resolver.process.targeting.Operator.TARGET_KEY; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; class FractionalTest { - @Test void selfContainedFractional() throws JsonLogicEvaluationException { + @Test + void selfContainedFractional() throws JsonLogicEvaluationException { // given Fractional fractional = new Fractional(); @@ -56,7 +58,8 @@ class FractionalTest { assertEquals("green", evaluate); } - @Test void targetingBackedFractional() throws JsonLogicEvaluationException { + @Test + void targetingBackedFractional() throws JsonLogicEvaluationException { // given Fractional fractional = new Fractional(); @@ -97,4 +100,121 @@ class FractionalTest { assertEquals("blue", evaluate); } + + @Test + void invalidRuleSumNot100() throws JsonLogicEvaluationException { + // given + Fractional fractional = new Fractional(); + + /* Rule + * [ + * [ + * "blue", + * 50 + * ], + * [ + * "green", + * 70 + * ] + * ] + * */ + + final List rule = new ArrayList<>(); + + final List bucket1 = new ArrayList<>(); + bucket1.add("blue"); + bucket1.add(50); + + final List bucket2 = new ArrayList<>(); + bucket2.add("green"); + bucket2.add(70); + + rule.add(bucket1); + rule.add(bucket2); + + Map data = new HashMap<>(); + data.put(FLAG_KEY, "headerColor"); + data.put(TARGET_KEY, "foo@foo.com"); + + // when + Object evaluate = fractional.evaluate(rule, data); + + // then + assertNull(evaluate); + } + + @Test + void notEnoughBuckets() throws JsonLogicEvaluationException { + // given + Fractional fractional = new Fractional(); + + /* Rule + * [ + * [ + * "blue", + * 100 + * ] + * ] + * */ + + final List rule = new ArrayList<>(); + + final List bucket1 = new ArrayList<>(); + bucket1.add("blue"); + bucket1.add(50); + + rule.add(bucket1); + + Map data = new HashMap<>(); + data.put(FLAG_KEY, "headerColor"); + data.put(TARGET_KEY, "foo@foo.com"); + + // when + Object evaluate = fractional.evaluate(rule, data); + + // then + assertNull(evaluate); + } + + + @Test + void invalidRule() throws JsonLogicEvaluationException { + // given + Fractional fractional = new Fractional(); + + /* Rule + * [ + * [ + * "blue", + * 50 + * ], + * [ + * "green" + * ] + * ] + * */ + + final List rule = new ArrayList<>(); + + final List bucket1 = new ArrayList<>(); + bucket1.add("blue"); + bucket1.add(50); + + final List bucket2 = new ArrayList<>(); + bucket2.add("green"); + + rule.add(bucket1); + rule.add(bucket2); + + Map data = new HashMap<>(); + data.put(FLAG_KEY, "headerColor"); + data.put(TARGET_KEY, "foo@foo.com"); + + // when + Object evaluate = fractional.evaluate(rule, data); + + // then + assertNull(evaluate); + } + } \ No newline at end of file From d6a2405a2f39c61bffb14d193327ff9ac168de37 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 13 Sep 2023 13:30:18 -0700 Subject: [PATCH 06/13] test fixes and unit tests Signed-off-by: Kavindu Dodanduwa --- .../resolver/process/targeting/SemVer.java | 2 +- .../process/targeting/FractionalTest.java | 49 ++++- .../process/targeting/OperatorTest.java | 172 ++++++++++++++++++ 3 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/OperatorTest.java diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java index 4ab9a9e6a..575445dba 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java @@ -67,7 +67,7 @@ public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationEx final String arg2Parsed = (String) arguments.get(1); if (!OPS.contains(arg2Parsed)) { - log.log(Level.FINE, String.format("Not valid operator in argument 2. Received: %a", arg2Parsed)); + log.log(Level.FINE, String.format("Not valid operator in argument 2. Received: %s", arg2Parsed)); return null; } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java index 2ae2dfca7..68b83df39 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java @@ -16,13 +16,13 @@ class FractionalTest { @Test - void selfContainedFractional() throws JsonLogicEvaluationException { + void selfContainedFractionalA() throws JsonLogicEvaluationException { // given Fractional fractional = new Fractional(); /* Rule * [ - * "bucketKey", + * "bucketKeyA", // this is resolved value of an expression * [ * "red", * 50 @@ -35,7 +35,7 @@ void selfContainedFractional() throws JsonLogicEvaluationException { * */ final List rule = new ArrayList<>(); - rule.add("bucketKey"); + rule.add("bucketKeyA"); final List bucket1 = new ArrayList<>(); bucket1.add("red"); @@ -58,6 +58,49 @@ void selfContainedFractional() throws JsonLogicEvaluationException { assertEquals("green", evaluate); } + @Test + void selfContainedFractionalB() throws JsonLogicEvaluationException { + // given + Fractional fractional = new Fractional(); + + /* Rule + * [ + * "bucketKeyB", // this is resolved value of an expression + * [ + * "red", + * 50 + * ], + * [ + * "blue", + * 50 + * ] + * ] + * */ + + final List rule = new ArrayList<>(); + rule.add("bucketKeyB"); + + final List bucket1 = new ArrayList<>(); + bucket1.add("red"); + bucket1.add(50); + + final List bucket2 = new ArrayList<>(); + bucket2.add("green"); + bucket2.add(50); + + rule.add(bucket1); + rule.add(bucket2); + + Map data = new HashMap<>(); + data.put(FLAG_KEY, "flagA"); + + // when + Object evaluate = fractional.evaluate(rule, data); + + // then + assertEquals("red", evaluate); + } + @Test void targetingBackedFractional() throws JsonLogicEvaluationException { // given diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/OperatorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/OperatorTest.java new file mode 100644 index 000000000..d091f1662 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/OperatorTest.java @@ -0,0 +1,172 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; + +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.Value; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class OperatorTest { + private static Operator OPERATOR; + + @BeforeAll + static void setUp() { + OPERATOR = new Operator(); + } + + @Test + void fractionalTestA() throws TargetingRuleException { + // given + + // fractional rule with email as expression key + final String targetingRule = "" + + "{\n" + + " \"fractional\": [\n" + + " {\"var\": \"email\"},\n" + + " [\n" + + " \"red\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"blue\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"green\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"yellow\",\n" + + " 25\n" + + " ]\n" + + " ]\n" + + "}"; + + Map ctxData = new HashMap<>(); + ctxData.put("email", new Value("rachel@faas.com")); + + + // when + Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); + + // then + assertEquals("red", evalVariant); + } + + @Test + void fractionalTestB() throws TargetingRuleException { + // given + + // fractional rule with email as expression key + final String targetingRule = "" + + "{\n" + + " \"fractional\": [\n" + + " {\"var\": \"email\"},\n" + + " [\n" + + " \"red\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"blue\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"green\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"yellow\",\n" + + " 25\n" + + " ]\n" + + " ]\n" + + "}"; + + Map ctxData = new HashMap<>(); + ctxData.put("email", new Value("monica@faas.com")); + + + // when + Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); + + // then + assertEquals("yellow", evalVariant); + } + + @Test + void stringCompStartsWith() throws TargetingRuleException { + // given + + // starts with rule with email as expression key + final String targetingRule = "" + + "{\n" + + " \"starts_with\": [\n" + + " {\"var\": \"email\"},\n" + + " \"admin\"\n" + + " ]\n" + + "}"; + + Map ctxData = new HashMap<>(); + ctxData.put("email", new Value("admin@faas.com")); + + + // when + Object evalVariant = OPERATOR.apply("adminRule", targetingRule, new ImmutableContext(ctxData)); + + // then + assertEquals(true, evalVariant); + } + + @Test + void stringCompEndsWith() throws TargetingRuleException { + // given + + // ends with rule with email as expression key + final String targetingRule = "" + + "{\n" + + " \"ends_with\": [\n" + + " {\"var\": \"email\"},\n" + + " \"@faas.com\"\n" + + " ]\n" + + "}"; + + Map ctxData = new HashMap<>(); + ctxData.put("email", new Value("admin@faas.com")); + + + // when + Object evalVariant = OPERATOR.apply("isFaas", targetingRule, new ImmutableContext(ctxData)); + + // then + assertEquals(true, evalVariant); + } + + @Test + void semVerA() throws TargetingRuleException { + // given + + // sem_ver rule with version as expression key + final String targetingRule = "{\n" + + " \"if\": [\n" + + " {\n" + + " \"sem_ver\": [{\"var\": \"version\"}, \">=\", \"1.0.0\"]\n" + + " },\n" + + " \"red\", null\n" + + " ]\n" + + "}"; + + Map ctxData = new HashMap<>(); + ctxData.put("version", new Value("1.1.0")); + + + // when + Object evalVariant = OPERATOR.apply("versionFlag", targetingRule, new ImmutableContext(ctxData)); + + // then + assertEquals("red", evalVariant); + } + +} \ No newline at end of file From bc3a4eb8028732c84d5a8353b3e7582accb105c9 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 13 Sep 2023 13:57:10 -0700 Subject: [PATCH 07/13] lint fixes Signed-off-by: Kavindu Dodanduwa --- checkstyle-suppressions.xml | 3 +++ .../resolver/process/model/FlagParser.java | 4 ++-- .../resolver/process/targeting/Operator.java | 21 +++++++++++-------- .../targeting/TargetingRuleException.java | 10 +++++++-- .../process/model/FlagParserTest.java | 3 ++- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 3d374f555..1890a6ec0 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -5,4 +5,7 @@ + + + \ No newline at end of file diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index b63e8fba9..811e24022 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -40,13 +40,13 @@ public class FlagParser { private static JsonSchema SCHEMA_VALIDATOR; static { - try( final InputStream schema = FlagParser.class.getClassLoader().getResourceAsStream(SCHEMA_RESOURCE)) { + try (final InputStream schema = FlagParser.class.getClassLoader().getResourceAsStream(SCHEMA_RESOURCE)) { if (schema == null) { log.log(Level.WARNING, String.format("Resource %s not found", SCHEMA_RESOURCE)); } else { final ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] buffer = new byte[512]; - for (int size; 0 <(size = schema.read(buffer));){ + for (int size; 0 < (size = schema.read(buffer)); ) { result.write(buffer, 0, size); } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java index d64df41f1..13352c698 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Operator.java @@ -13,8 +13,8 @@ */ public class Operator { - final static String FLAG_KEY = "$flagKey"; - final static String TARGET_KEY = "targetingKey"; + static final String FLAG_KEY = "$flagKey"; + static final String TARGET_KEY = "targetingKey"; private final JsonLogic jsonLogicHandler; @@ -29,6 +29,9 @@ public Operator() { jsonLogicHandler.addOperation(new StringComp(StringComp.Type.ENDS_WITH)); } + /** + * Apply this operator on the provided rule. + */ public Object apply(final String flagKey, final String targetingRule, final EvaluationContext ctx) throws TargetingRuleException { final Map valueMap = ctx.asObjectMap(); @@ -50,18 +53,18 @@ static class FlagProperties { if (from instanceof Map) { Map dataMap = (Map) from; - Object fKey = dataMap.get(FLAG_KEY); + Object flagKey = dataMap.get(FLAG_KEY); - if (fKey instanceof String) { - flagKey = (String) fKey; + if (flagKey instanceof String) { + this.flagKey = (String) flagKey; } else { - flagKey = null; + this.flagKey = null; } - Object tKey = dataMap.get(TARGET_KEY); + Object targetKey = dataMap.get(TARGET_KEY); - if (tKey instanceof String) { - targetingKey = (String) tKey; + if (targetKey instanceof String) { + targetingKey = (String) targetKey; } else { targetingKey = null; } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/TargetingRuleException.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/TargetingRuleException.java index 8b7f876bc..55bfccc9d 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/TargetingRuleException.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/TargetingRuleException.java @@ -1,8 +1,14 @@ package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; -public class TargetingRuleException extends Exception{ +/** + * Exception used by targeting rule package. + **/ +public class TargetingRuleException extends Exception { - public TargetingRuleException(final String message, final Throwable t){ + /** + * Construct exception. + **/ + public TargetingRuleException(final String message, final Throwable t) { super(message, t); } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java index 78d9bf02d..3229e35f1 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java @@ -46,7 +46,8 @@ public void validJsonConfigurationWithTargetingRulesParsing() throws IOException assertEquals("loop", variants.get("loop")); assertEquals("binet", variants.get("binet")); - assertEquals("{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}", stringFlag.getTargeting()); + assertEquals("{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}", + stringFlag.getTargeting()); } From 266a3706db2741fa933a7188a095536019662238 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 13 Sep 2023 14:05:00 -0700 Subject: [PATCH 08/13] fix pmd violations Signed-off-by: Kavindu Dodanduwa --- .../flagd/resolver/process/model/FlagParser.java | 2 +- .../flagd/resolver/process/targeting/Fractional.java | 2 +- .../flagd/resolver/process/targeting/SemVer.java | 8 +++----- .../flagd/resolver/process/targeting/lib/Murmur3.java | 7 +------ 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index 811e24022..8194548b4 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -40,7 +40,7 @@ public class FlagParser { private static JsonSchema SCHEMA_VALIDATOR; static { - try (final InputStream schema = FlagParser.class.getClassLoader().getResourceAsStream(SCHEMA_RESOURCE)) { + try (InputStream schema = FlagParser.class.getClassLoader().getResourceAsStream(SCHEMA_RESOURCE)) { if (schema == null) { log.log(Level.WARNING, String.format("Resource %s not found", SCHEMA_RESOURCE)); } else { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java index 45bd176fe..5857ad46b 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java @@ -119,7 +119,7 @@ private static class FractionProperty { throw new JsonLogicException("Second element of the fraction property is not a number"); } - variant = ((String) array.get(0)); + variant = (String) array.get(0); percentage = ((Number) array.get(1)).doubleValue(); } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java index 575445dba..4df8efe60 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/SemVer.java @@ -47,12 +47,10 @@ public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationEx } for (int i = 0; i < 3; i++) { - if (arguments.get(i) instanceof String) { - continue; + if (!(arguments.get(i) instanceof String)) { + log.log(Level.FINE, "Invalid argument type. Require Strings"); + return null; } - - log.log(Level.FINE, "Invalid argument type. Require Strings"); - return null; } // arg 1 should be a SemVer diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java index b383574d0..60797f14e 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java @@ -37,8 +37,7 @@ package dev.openfeature.contrib.providers.flagd.resolver.process.targeting.lib; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - +@SuppressWarnings("PMD") public class Murmur3 { // from 64-bit linear congruential generator public static final long NULL_HASHCODE = 2862933555777941757L; @@ -142,7 +141,6 @@ public static int hash32(byte[] data, int length, int seed) { * @param seed - seed. (default 0) * @return - hashcode */ - @SuppressFBWarnings(value = {"SF_SWITCH_FALLTHROUGH", "SF_SWITCH_NO_DEFAULT"}, justification = "Expected") public static int hash32(byte[] data, int offset, int length, int seed) { int hash = seed; final int nblocks = length >> 2; @@ -266,7 +264,6 @@ public static long hash64(byte[] data, int offset, int length) { * @param seed - seed. (default is 0) * @return - hashcode */ - @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "Expected") public static long hash64(byte[] data, int offset, int length, int seed) { long hash = seed; final int nblocks = length >> 3; @@ -341,7 +338,6 @@ public static long[] hash128(byte[] data) { * @param seed - seed. (default is 0) * @return - hashcode (2 longs) */ - @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "Expected") public static long[] hash128(byte[] data, int offset, int length, int seed) { long h1 = seed; long h2 = seed; @@ -522,7 +518,6 @@ public final void add(byte[] data, int offset, int length) { System.arraycopy(data, offset + consumed, tail, 0, tailLen); } - @SuppressFBWarnings(value = {"SF_SWITCH_FALLTHROUGH", "SF_SWITCH_NO_DEFAULT"}, justification = "Expected") public final int end() { int k1 = 0; switch (tailLen) { From b06e200159ee84df739bb212d31da455f3018bc9 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 13 Sep 2023 14:17:58 -0700 Subject: [PATCH 09/13] fix spotbug suggestions Signed-off-by: Kavindu Dodanduwa --- .../flagd/resolver/process/model/FlagParser.java | 3 ++- .../flagd/resolver/process/targeting/lib/Murmur3.java | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index 8194548b4..ad06ef628 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -13,6 +13,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -51,7 +52,7 @@ public class FlagParser { } JsonSchemaFactory instance = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); - SCHEMA_VALIDATOR = instance.getSchema(result.toString()); + SCHEMA_VALIDATOR = instance.getSchema(result.toString("UTF-8")); } } catch (Throwable e) { // log only, do not throw diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java index 60797f14e..2d5e9a435 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java @@ -37,6 +37,8 @@ package dev.openfeature.contrib.providers.flagd.resolver.process.targeting.lib; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + @SuppressWarnings("PMD") public class Murmur3 { // from 64-bit linear congruential generator @@ -141,6 +143,7 @@ public static int hash32(byte[] data, int length, int seed) { * @param seed - seed. (default 0) * @return - hashcode */ + @SuppressFBWarnings(value = {"SF_SWITCH_FALLTHROUGH", "SF_SWITCH_NO_DEFAULT"}, justification = "Expected") public static int hash32(byte[] data, int offset, int length, int seed) { int hash = seed; final int nblocks = length >> 2; @@ -264,6 +267,7 @@ public static long hash64(byte[] data, int offset, int length) { * @param seed - seed. (default is 0) * @return - hashcode */ + @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "Expected") public static long hash64(byte[] data, int offset, int length, int seed) { long hash = seed; final int nblocks = length >> 3; @@ -338,6 +342,7 @@ public static long[] hash128(byte[] data) { * @param seed - seed. (default is 0) * @return - hashcode (2 longs) */ + @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "Expected") public static long[] hash128(byte[] data, int offset, int length, int seed) { long h1 = seed; long h2 = seed; @@ -518,6 +523,7 @@ public final void add(byte[] data, int offset, int length) { System.arraycopy(data, offset + consumed, tail, 0, tailLen); } + @SuppressFBWarnings(value = {"SF_SWITCH_FALLTHROUGH", "SF_SWITCH_NO_DEFAULT"}, justification = "Expected") public final int end() { int k1 = 0; switch (tailLen) { @@ -549,4 +555,4 @@ public final int end() { private static int orBytes(byte b1, byte b2, byte b3, byte b4) { return (b1 & 0xff) | ((b2 & 0xff) << 8) | ((b3 & 0xff) << 16) | ((b4 & 0xff) << 24); } -} \ No newline at end of file +} From d19bbaeff7f691035ff0274837671327be6cdaa3 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 13 Sep 2023 14:23:40 -0700 Subject: [PATCH 10/13] fix pmd Signed-off-by: Kavindu Dodanduwa --- .../providers/flagd/resolver/process/model/FlagParser.java | 1 - 1 file changed, 1 deletion(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index ad06ef628..7a916b813 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -13,7 +13,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; import java.util.HashMap; import java.util.Iterator; import java.util.Map; From 2386d16c42a670f2f1a83618fe6e19a2d12776ab Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Thu, 14 Sep 2023 10:37:41 -0700 Subject: [PATCH 11/13] fix fractional hash calculation Signed-off-by: Kavindu Dodanduwa --- .../process/targeting/Fractional.java | 10 ++--- .../process/targeting/OperatorTest.java | 43 ++++++++++++++++++- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java index 5857ad46b..91b4f06f0 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java @@ -77,10 +77,10 @@ public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationEx private static String distributeValue(final String hashKey, final List propertyList) throws JsonLogicEvaluationException { byte[] bytes = hashKey.getBytes(StandardCharsets.UTF_8); - double bucket = - (Math.abs(Murmur3.hash64(bytes, 0, bytes.length, 0)) * 1.0d / Long.MAX_VALUE) * 100.0d; + int mmrHash = Murmur3.hash32(bytes, 0, bytes.length, 0); + int bucket = (int) ((Math.abs(mmrHash) * 1.0f / Integer.MAX_VALUE) * 100); - double bucketSum = 0; + int bucketSum = 0; for (FractionProperty p : propertyList) { bucketSum += p.getPercentage(); @@ -96,7 +96,7 @@ private static String distributeValue(final String hashKey, final List)) { @@ -120,7 +120,7 @@ private static class FractionProperty { } variant = (String) array.get(0); - percentage = ((Number) array.get(1)).doubleValue(); + percentage = ((Number) array.get(1)).intValue(); } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/OperatorTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/OperatorTest.java index d091f1662..a6a1fe2e3 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/OperatorTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/OperatorTest.java @@ -54,7 +54,7 @@ void fractionalTestA() throws TargetingRuleException { Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); // then - assertEquals("red", evalVariant); + assertEquals("yellow", evalVariant); } @Test @@ -93,7 +93,46 @@ void fractionalTestB() throws TargetingRuleException { Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); // then - assertEquals("yellow", evalVariant); + assertEquals("blue", evalVariant); + } + + @Test + void fractionalTestC() throws TargetingRuleException { + // given + + // fractional rule with email as expression key + final String targetingRule = "" + + "{\n" + + " \"fractional\": [\n" + + " {\"var\": \"email\"},\n" + + " [\n" + + " \"red\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"blue\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"green\",\n" + + " 25\n" + + " ],\n" + + " [\n" + + " \"yellow\",\n" + + " 25\n" + + " ]\n" + + " ]\n" + + "}"; + + Map ctxData = new HashMap<>(); + ctxData.put("email", new Value("joey@faas.com")); + + + // when + Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); + + // then + assertEquals("red", evalVariant); } @Test From ba135f0af6114fb97c7e680213cd29da2fadd531 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Thu, 14 Sep 2023 11:37:21 -0700 Subject: [PATCH 12/13] fix tests Signed-off-by: Kavindu Dodanduwa --- .../flagd/resolver/process/targeting/FractionalTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java index 68b83df39..b3220e1e0 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/FractionalTest.java @@ -55,7 +55,7 @@ void selfContainedFractionalA() throws JsonLogicEvaluationException { Object evaluate = fractional.evaluate(rule, data); // then - assertEquals("green", evaluate); + assertEquals("red", evaluate); } @Test @@ -98,7 +98,7 @@ void selfContainedFractionalB() throws JsonLogicEvaluationException { Object evaluate = fractional.evaluate(rule, data); // then - assertEquals("red", evaluate); + assertEquals("green", evaluate); } @Test From f3354a9bcfb8231dadf6b4ea590012d7add02783 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Thu, 14 Sep 2023 11:53:03 -0700 Subject: [PATCH 13/13] use apache common murmur3 Signed-off-by: Kavindu Dodanduwa --- checkstyle-suppressions.xml | 3 - .../process/targeting/Fractional.java | 4 +- .../process/targeting/lib/Murmur3.java | 558 ------------------ 3 files changed, 2 insertions(+), 563 deletions(-) delete mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 1890a6ec0..3d374f555 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -5,7 +5,4 @@ - - - \ No newline at end of file diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java index 91b4f06f0..354201cf4 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/Fractional.java @@ -1,11 +1,11 @@ package dev.openfeature.contrib.providers.flagd.resolver.process.targeting; -import dev.openfeature.contrib.providers.flagd.resolver.process.targeting.lib.Murmur3; import io.github.jamsesso.jsonlogic.JsonLogicException; import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; import io.github.jamsesso.jsonlogic.evaluator.expressions.PreEvaluatedArgumentsExpression; import lombok.Getter; import lombok.extern.java.Log; +import org.apache.commons.codec.digest.MurmurHash3; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -77,7 +77,7 @@ public Object evaluate(List arguments, Object data) throws JsonLogicEvaluationEx private static String distributeValue(final String hashKey, final List propertyList) throws JsonLogicEvaluationException { byte[] bytes = hashKey.getBytes(StandardCharsets.UTF_8); - int mmrHash = Murmur3.hash32(bytes, 0, bytes.length, 0); + int mmrHash = MurmurHash3.hash32x86(bytes, 0, bytes.length, 0); int bucket = (int) ((Math.abs(mmrHash) * 1.0f / Integer.MAX_VALUE) * 100); int bucketSum = 0; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java deleted file mode 100644 index 2d5e9a435..000000000 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/targeting/lib/Murmur3.java +++ /dev/null @@ -1,558 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Murmur3 is successor to Murmur2 fast non-crytographic hash algorithms. - * - * Murmur3 32 and 128 bit variants. - * 32-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp#94 - * 128-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp#255 - * - * This is a public domain code with no copyrights. - * From homepage of MurmurHash (https://code.google.com/p/smhasher/), - * "All MurmurHash versions are public domain software, and the author disclaims all copyright - * to their code." - * - * This code is burrowed from Apache Hive (https://github.com/apache/hive) - * - * @see - * Apache Hive Murmer3 - */ - -package dev.openfeature.contrib.providers.flagd.resolver.process.targeting.lib; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -@SuppressWarnings("PMD") -public class Murmur3 { - // from 64-bit linear congruential generator - public static final long NULL_HASHCODE = 2862933555777941757L; - - // Constants for 32 bit variant - private static final int C1_32 = 0xcc9e2d51; - private static final int C2_32 = 0x1b873593; - private static final int R1_32 = 15; - private static final int R2_32 = 13; - private static final int M_32 = 5; - private static final int N_32 = 0xe6546b64; - - // Constants for 128 bit variant - private static final long C1 = 0x87c37b91114253d5L; - private static final long C2 = 0x4cf5ad432745937fL; - private static final int R1 = 31; - private static final int R2 = 27; - private static final int R3 = 33; - private static final int M = 5; - private static final int N1 = 0x52dce729; - private static final int N2 = 0x38495ab5; - - public static final int DEFAULT_SEED = 104729; - - public static int hash32(long l0, long l1) { - return hash32(l0, l1, DEFAULT_SEED); - } - - public static int hash32(long l0) { - return hash32(l0, DEFAULT_SEED); - } - - /** - * Murmur3 32-bit variant. - */ - public static int hash32(long l0, int seed) { - int hash = seed; - final long r0 = Long.reverseBytes(l0); - - hash = mix32((int) r0, hash); - hash = mix32((int) (r0 >>> 32), hash); - - return fmix32(Long.BYTES, hash); - } - - /** - * Murmur3 32-bit variant. - */ - public static int hash32(long l0, long l1, int seed) { - int hash = seed; - final long r0 = Long.reverseBytes(l0); - final long r1 = Long.reverseBytes(l1); - - hash = mix32((int) r0, hash); - hash = mix32((int) (r0 >>> 32), hash); - hash = mix32((int) (r1), hash); - hash = mix32((int) (r1 >>> 32), hash); - - return fmix32(Long.BYTES * 2, hash); - } - - /** - * Murmur3 32-bit variant. - * - * @param data - input byte array - * @return - hashcode - */ - public static int hash32(byte[] data) { - return hash32(data, 0, data.length, DEFAULT_SEED); - } - - /** - * Murmur3 32-bit variant. - * - * @param data - input byte array - * @param length - length of array - * @return - hashcode - */ - public static int hash32(byte[] data, int length) { - return hash32(data, 0, length, DEFAULT_SEED); - } - - /** - * Murmur3 32-bit variant. - * - * @param data - input byte array - * @param length - length of array - * @param seed - seed. (default 0) - * @return - hashcode - */ - public static int hash32(byte[] data, int length, int seed) { - return hash32(data, 0, length, seed); - } - - /** - * Murmur3 32-bit variant. - * - * @param data - input byte array - * @param offset - offset of data - * @param length - length of array - * @param seed - seed. (default 0) - * @return - hashcode - */ - @SuppressFBWarnings(value = {"SF_SWITCH_FALLTHROUGH", "SF_SWITCH_NO_DEFAULT"}, justification = "Expected") - public static int hash32(byte[] data, int offset, int length, int seed) { - int hash = seed; - final int nblocks = length >> 2; - - // body - for (int i = 0; i < nblocks; i++) { - int i_4 = i << 2; - int k = (data[offset + i_4] & 0xff) - | ((data[offset + i_4 + 1] & 0xff) << 8) - | ((data[offset + i_4 + 2] & 0xff) << 16) - | ((data[offset + i_4 + 3] & 0xff) << 24); - - hash = mix32(k, hash); - } - - // tail - int idx = nblocks << 2; - int k1 = 0; - switch (length - idx) { - case 3: - k1 ^= data[offset + idx + 2] << 16; - case 2: - k1 ^= data[offset + idx + 1] << 8; - case 1: - k1 ^= data[offset + idx]; - - // mix functions - k1 *= C1_32; - k1 = Integer.rotateLeft(k1, R1_32); - k1 *= C2_32; - hash ^= k1; - } - - return fmix32(length, hash); - } - - private static int mix32(int k, int hash) { - k *= C1_32; - k = Integer.rotateLeft(k, R1_32); - k *= C2_32; - hash ^= k; - return Integer.rotateLeft(hash, R2_32) * M_32 + N_32; - } - - private static int fmix32(int length, int hash) { - hash ^= length; - hash ^= (hash >>> 16); - hash *= 0x85ebca6b; - hash ^= (hash >>> 13); - hash *= 0xc2b2ae35; - hash ^= (hash >>> 16); - - return hash; - } - - /** - * Murmur3 64-bit variant. This is essentially MSB 8 bytes of Murmur3 128-bit variant. - * - * @param data - input byte array - * @return - hashcode - */ - public static long hash64(byte[] data) { - return hash64(data, 0, data.length, DEFAULT_SEED); - } - - public static long hash64(long data) { - long hash = DEFAULT_SEED; - long k = Long.reverseBytes(data); - int length = Long.BYTES; - // mix functions - k *= C1; - k = Long.rotateLeft(k, R1); - k *= C2; - hash ^= k; - hash = Long.rotateLeft(hash, R2) * M + N1; - // finalization - hash ^= length; - hash = fmix64(hash); - return hash; - } - - public static long hash64(int data) { - long k1 = Integer.reverseBytes(data) & (-1L >>> 32); - int length = Integer.BYTES; - long hash = DEFAULT_SEED; - k1 *= C1; - k1 = Long.rotateLeft(k1, R1); - k1 *= C2; - hash ^= k1; - // finalization - hash ^= length; - hash = fmix64(hash); - return hash; - } - - public static long hash64(short data) { - long hash = DEFAULT_SEED; - long k1 = 0; - k1 ^= ((long) data & 0xff) << 8; - k1 ^= ((long)((data & 0xFF00) >> 8) & 0xff); - k1 *= C1; - k1 = Long.rotateLeft(k1, R1); - k1 *= C2; - hash ^= k1; - - // finalization - hash ^= Short.BYTES; - hash = fmix64(hash); - return hash; - } - - public static long hash64(byte[] data, int offset, int length) { - return hash64(data, offset, length, DEFAULT_SEED); - } - - /** - * Murmur3 64-bit variant. This is essentially MSB 8 bytes of Murmur3 128-bit variant. - * - * @param data - input byte array - * @param length - length of array - * @param seed - seed. (default is 0) - * @return - hashcode - */ - @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "Expected") - public static long hash64(byte[] data, int offset, int length, int seed) { - long hash = seed; - final int nblocks = length >> 3; - - // body - for (int i = 0; i < nblocks; i++) { - final int i8 = i << 3; - long k = ((long) data[offset + i8] & 0xff) - | (((long) data[offset + i8 + 1] & 0xff) << 8) - | (((long) data[offset + i8 + 2] & 0xff) << 16) - | (((long) data[offset + i8 + 3] & 0xff) << 24) - | (((long) data[offset + i8 + 4] & 0xff) << 32) - | (((long) data[offset + i8 + 5] & 0xff) << 40) - | (((long) data[offset + i8 + 6] & 0xff) << 48) - | (((long) data[offset + i8 + 7] & 0xff) << 56); - - // mix functions - k *= C1; - k = Long.rotateLeft(k, R1); - k *= C2; - hash ^= k; - hash = Long.rotateLeft(hash, R2) * M + N1; - } - - // tail - long k1 = 0; - int tailStart = nblocks << 3; - switch (length - tailStart) { - case 7: - k1 ^= ((long) data[offset + tailStart + 6] & 0xff) << 48; - case 6: - k1 ^= ((long) data[offset + tailStart + 5] & 0xff) << 40; - case 5: - k1 ^= ((long) data[offset + tailStart + 4] & 0xff) << 32; - case 4: - k1 ^= ((long) data[offset + tailStart + 3] & 0xff) << 24; - case 3: - k1 ^= ((long) data[offset + tailStart + 2] & 0xff) << 16; - case 2: - k1 ^= ((long) data[offset + tailStart + 1] & 0xff) << 8; - case 1: - k1 ^= ((long) data[offset + tailStart] & 0xff); - k1 *= C1; - k1 = Long.rotateLeft(k1, R1); - k1 *= C2; - hash ^= k1; - } - - // finalization - hash ^= length; - hash = fmix64(hash); - - return hash; - } - - /** - * Murmur3 128-bit variant. - * - * @param data - input byte array - * @return - hashcode (2 longs) - */ - public static long[] hash128(byte[] data) { - return hash128(data, 0, data.length, DEFAULT_SEED); - } - - /** - * Murmur3 128-bit variant. - * - * @param data - input byte array - * @param offset - the first element of array - * @param length - length of array - * @param seed - seed. (default is 0) - * @return - hashcode (2 longs) - */ - @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "Expected") - public static long[] hash128(byte[] data, int offset, int length, int seed) { - long h1 = seed; - long h2 = seed; - final int nblocks = length >> 4; - - // body - for (int i = 0; i < nblocks; i++) { - final int i16 = i << 4; - long k1 = ((long) data[offset + i16] & 0xff) - | (((long) data[offset + i16 + 1] & 0xff) << 8) - | (((long) data[offset + i16 + 2] & 0xff) << 16) - | (((long) data[offset + i16 + 3] & 0xff) << 24) - | (((long) data[offset + i16 + 4] & 0xff) << 32) - | (((long) data[offset + i16 + 5] & 0xff) << 40) - | (((long) data[offset + i16 + 6] & 0xff) << 48) - | (((long) data[offset + i16 + 7] & 0xff) << 56); - - long k2 = ((long) data[offset + i16 + 8] & 0xff) - | (((long) data[offset + i16 + 9] & 0xff) << 8) - | (((long) data[offset + i16 + 10] & 0xff) << 16) - | (((long) data[offset + i16 + 11] & 0xff) << 24) - | (((long) data[offset + i16 + 12] & 0xff) << 32) - | (((long) data[offset + i16 + 13] & 0xff) << 40) - | (((long) data[offset + i16 + 14] & 0xff) << 48) - | (((long) data[offset + i16 + 15] & 0xff) << 56); - - // mix functions for k1 - k1 *= C1; - k1 = Long.rotateLeft(k1, R1); - k1 *= C2; - h1 ^= k1; - h1 = Long.rotateLeft(h1, R2); - h1 += h2; - h1 = h1 * M + N1; - - // mix functions for k2 - k2 *= C2; - k2 = Long.rotateLeft(k2, R3); - k2 *= C1; - h2 ^= k2; - h2 = Long.rotateLeft(h2, R1); - h2 += h1; - h2 = h2 * M + N2; - } - - // tail - long k1 = 0; - long k2 = 0; - int tailStart = nblocks << 4; - switch (length - tailStart) { - case 15: - k2 ^= (long) (data[offset + tailStart + 14] & 0xff) << 48; - case 14: - k2 ^= (long) (data[offset + tailStart + 13] & 0xff) << 40; - case 13: - k2 ^= (long) (data[offset + tailStart + 12] & 0xff) << 32; - case 12: - k2 ^= (long) (data[offset + tailStart + 11] & 0xff) << 24; - case 11: - k2 ^= (long) (data[offset + tailStart + 10] & 0xff) << 16; - case 10: - k2 ^= (long) (data[offset + tailStart + 9] & 0xff) << 8; - case 9: - k2 ^= (long) (data[offset + tailStart + 8] & 0xff); - k2 *= C2; - k2 = Long.rotateLeft(k2, R3); - k2 *= C1; - h2 ^= k2; - - case 8: - k1 ^= (long) (data[offset + tailStart + 7] & 0xff) << 56; - case 7: - k1 ^= (long) (data[offset + tailStart + 6] & 0xff) << 48; - case 6: - k1 ^= (long) (data[offset + tailStart + 5] & 0xff) << 40; - case 5: - k1 ^= (long) (data[offset + tailStart + 4] & 0xff) << 32; - case 4: - k1 ^= (long) (data[offset + tailStart + 3] & 0xff) << 24; - case 3: - k1 ^= (long) (data[offset + tailStart + 2] & 0xff) << 16; - case 2: - k1 ^= (long) (data[offset + tailStart + 1] & 0xff) << 8; - case 1: - k1 ^= (long) (data[offset + tailStart] & 0xff); - k1 *= C1; - k1 = Long.rotateLeft(k1, R1); - k1 *= C2; - h1 ^= k1; - } - - // finalization - h1 ^= length; - h2 ^= length; - - h1 += h2; - h2 += h1; - - h1 = fmix64(h1); - h2 = fmix64(h2); - - h1 += h2; - h2 += h1; - - return new long[]{h1, h2}; - } - - private static long fmix64(long h) { - h ^= (h >>> 33); - h *= 0xff51afd7ed558ccdL; - h ^= (h >>> 33); - h *= 0xc4ceb9fe1a85ec53L; - h ^= (h >>> 33); - return h; - } - - public static class IncrementalHash32 { - byte[] tail = new byte[3]; - int tailLen; - int totalLen; - int hash; - - public final void start(int hash) { - tailLen = totalLen = 0; - this.hash = hash; - } - - public final void add(byte[] data, int offset, int length) { - if (length == 0) return; - totalLen += length; - if (tailLen + length < 4) { - System.arraycopy(data, offset, tail, tailLen, length); - tailLen += length; - return; - } - int offset2 = 0; - if (tailLen > 0) { - offset2 = (4 - tailLen); - int k = -1; - switch (tailLen) { - case 1: - k = orBytes(tail[0], data[offset], data[offset + 1], data[offset + 2]); - break; - case 2: - k = orBytes(tail[0], tail[1], data[offset], data[offset + 1]); - break; - case 3: - k = orBytes(tail[0], tail[1], tail[2], data[offset]); - break; - default: throw new AssertionError(tailLen); - } - // mix functions - k *= C1_32; - k = Integer.rotateLeft(k, R1_32); - k *= C2_32; - hash ^= k; - hash = Integer.rotateLeft(hash, R2_32) * M_32 + N_32; - } - int length2 = length - offset2; - offset += offset2; - final int nblocks = length2 >> 2; - - for (int i = 0; i < nblocks; i++) { - int i_4 = (i << 2) + offset; - int k = orBytes(data[i_4], data[i_4 + 1], data[i_4 + 2], data[i_4 + 3]); - - // mix functions - k *= C1_32; - k = Integer.rotateLeft(k, R1_32); - k *= C2_32; - hash ^= k; - hash = Integer.rotateLeft(hash, R2_32) * M_32 + N_32; - } - - int consumed = (nblocks << 2); - tailLen = length2 - consumed; - if (consumed == length2) return; - System.arraycopy(data, offset + consumed, tail, 0, tailLen); - } - - @SuppressFBWarnings(value = {"SF_SWITCH_FALLTHROUGH", "SF_SWITCH_NO_DEFAULT"}, justification = "Expected") - public final int end() { - int k1 = 0; - switch (tailLen) { - case 3: - k1 ^= tail[2] << 16; - case 2: - k1 ^= tail[1] << 8; - case 1: - k1 ^= tail[0]; - - // mix functions - k1 *= C1_32; - k1 = Integer.rotateLeft(k1, R1_32); - k1 *= C2_32; - hash ^= k1; - } - - // finalization - hash ^= totalLen; - hash ^= (hash >>> 16); - hash *= 0x85ebca6b; - hash ^= (hash >>> 13); - hash *= 0xc2b2ae35; - hash ^= (hash >>> 16); - return hash; - } - } - - private static int orBytes(byte b1, byte b2, byte b3, byte b4) { - return (b1 & 0xff) | ((b2 & 0xff) << 8) | ((b3 & 0xff) << 16) | ((b4 & 0xff) << 24); - } -}