From 1b7e5ca546d9335570aaca569678396a646d5fe8 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 24 Jan 2025 09:21:49 -0800 Subject: [PATCH 1/2] FixEndian --- .../management/filters/TargetingFilter.java | 41 +++-------------- .../implementation/FeatureFilterUtils.java | 44 +++++++++++++++++++ .../filters/TargetingFilterTest.java | 4 +- .../TargetingFilterUtilsTest.java | 20 +++++++++ 4 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index bea9aa4879db..7028570729d9 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -2,21 +2,17 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.filters; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; +import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; import com.azure.spring.cloud.feature.management.implementation.targeting.Audience; import com.azure.spring.cloud.feature.management.implementation.targeting.Exclusion; import com.azure.spring.cloud.feature.management.implementation.targeting.GroupRollout; @@ -133,8 +129,10 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { Audience audience; String exclusionValue = FeatureFilterUtils.getKeyCase(parameters, EXCLUSION_CAMEL); - String exclusionUserValue = FeatureFilterUtils.getKeyCase((Map) parameters.get(exclusionValue), "Users"); - String exclusionGroupsValue = FeatureFilterUtils.getKeyCase((Map) parameters.get(exclusionValue), "Groups"); + String exclusionUserValue = FeatureFilterUtils.getKeyCase((Map) parameters.get(exclusionValue), + "Users"); + String exclusionGroupsValue = FeatureFilterUtils + .getKeyCase((Map) parameters.get(exclusionValue), "Groups"); if (((Map) parameters.getOrDefault(exclusionValue, new HashMap<>())) .get(exclusionUserValue) instanceof List) { @@ -227,35 +225,8 @@ private boolean validateTargetingContext(TargetingFilterContext targetingContext return (!hasUserDefined && !(hasGroupsDefined && hasAtLeastOneGroup)); } - /** - * Computes the percentage that the contextId falls into. - * - * @param contextId Id of the context being targeted - * @return the bucket value of the context id - * @throws TargetingException Unable to create hash of target context - */ - protected double isTargetedPercentage(String contextId) { - byte[] hash = null; - - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - hash = digest.digest(contextId.getBytes(Charset.defaultCharset())); - } catch (NoSuchAlgorithmException e) { - throw new TargetingException("Unable to find SHA-256 for targeting.", e); - } - - if (hash == null) { - throw new TargetingException("Unable to create Targeting Hash for " + contextId); - } - - ByteBuffer wrapped = ByteBuffer.wrap(hash); - int contextMarker = Math.abs(wrapped.getInt()); - - return (contextMarker / (double) Integer.MAX_VALUE) * 100; - } - private boolean isTargeted(String contextId, double percentage) { - return isTargetedPercentage(contextId) < percentage; + return FeatureFilterUtils.isTargetedPercentage(contextId) < percentage; } /** diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index a8631bfc9366..1598ba306ba8 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -3,11 +3,16 @@ package com.azure.spring.cloud.feature.management.implementation; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Map; import org.springframework.util.StringUtils; +import com.azure.spring.cloud.feature.management.models.TargetingException; + public class FeatureFilterUtils { /** @@ -34,4 +39,43 @@ public static String getKeyCase(Map parameters, String key) { return StringUtils.uncapitalize(key); } + /** + * Computes the percentage that the contextId falls into. + * + * @param contextId Id of the context being targeted + * @return the bucket value of the context id + * @throws TargetingException Unable to create hash of target context + */ + public static double isTargetedPercentage(String contextId) { + byte[] hash = null; + if (contextId == null) { + contextId = "\n"; + } + + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + hash = digest.digest(contextId.getBytes()); + + } catch (NoSuchAlgorithmException e) { + throw new TargetingException("Unable to find SHA-256 for targeting.", e); + } + + if (hash == null) { + throw new TargetingException("Unable to create Targeting Hash for " + contextId); + } + + BigInteger bi = fromLittleEndianByteArray(hash); + + return (bi.longValue() / (Math.pow(2, 32) - 1)) * 100; + } + + public static BigInteger fromLittleEndianByteArray(byte[] bytes) { + byte[] reversedBytes = new byte[4]; + for (int i = 0; i < 4; i++) { + reversedBytes[i] = bytes[3 - i]; + } + + return new BigInteger(1, reversedBytes); + } + } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java index 15b4c271da7e..ed45ced91d9c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java @@ -218,7 +218,7 @@ public void targetedGroupFiftyPass() { TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Jane", targetedGroups)); - assertTrue(filter.evaluate(context)); + assertFalse(filter.evaluate(context)); } @Test @@ -246,7 +246,7 @@ public void targetedGroupFiftyFalse() { TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", targetedGroups)); - assertFalse(filter.evaluate(context)); + assertTrue(filter.evaluate(context)); } @Test diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java new file mode 100644 index 000000000000..ab85d8a7c543 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java @@ -0,0 +1,20 @@ +package com.azure.spring.cloud.feature.management.implemenation; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.azure.spring.cloud.feature.management.implementation.FeatureFilterUtils; + +public class TargetingFilterUtilsTest { + + @Test + public void isTargetedPercentageTest() { + assertEquals(FeatureFilterUtils.isTargetedPercentage(null), 9.875071074318855); + assertEquals(FeatureFilterUtils.isTargetedPercentage(""), 26.0813765987012); + assertEquals(FeatureFilterUtils.isTargetedPercentage("Alice"), 38.306839656621875); + assertEquals(FeatureFilterUtils.isTargetedPercentage("Quinn\nDeb"), 38.306839656621875); + assertEquals(FeatureFilterUtils.isTargetedPercentage("\nProd"), 79.98622464481421); + } + +} From 4e82e1cef2915170f80a463de06d5643db72819f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 3 Feb 2025 09:14:03 -0800 Subject: [PATCH 2/2] rename method --- .../management/implementation/FeatureFilterUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index 1598ba306ba8..90deefc80b0c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -64,15 +64,15 @@ public static double isTargetedPercentage(String contextId) { throw new TargetingException("Unable to create Targeting Hash for " + contextId); } - BigInteger bi = fromLittleEndianByteArray(hash); + BigInteger bi = bigEndianToLittleEndian(hash); return (bi.longValue() / (Math.pow(2, 32) - 1)) * 100; } - public static BigInteger fromLittleEndianByteArray(byte[] bytes) { + public static BigInteger bigEndianToLittleEndian(byte[] bigEndian) { byte[] reversedBytes = new byte[4]; for (int i = 0; i < 4; i++) { - reversedBytes[i] = bytes[3 - i]; + reversedBytes[i] = bigEndian[3 - i]; } return new BigInteger(1, reversedBytes);