diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/ConfigurationSettingJsonDeserializer.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/ConfigurationSettingJsonDeserializer.java index 7d7c3a9174b71..3d1c9c8bad1a1 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/ConfigurationSettingJsonDeserializer.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/ConfigurationSettingJsonDeserializer.java @@ -66,6 +66,12 @@ public final class ConfigurationSettingJsonDeserializer extends JsonDeserializer configurationSettingSubclassDeserializer(FeatureFlagConfigurationSetting.class)); } + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson + * ObjectMapper. + * + * @return a simple module to be plugged onto Jackson ObjectMapper. + */ public static SimpleModule getModule() { return MODULE; } @@ -113,17 +119,8 @@ private static SecretReferenceConfigurationSetting readSecretReferenceConfigurat settingValue = valueNode.asText(); } - final JsonNode settingValueNode = toJsonNode(settingValue); - - final JsonNode uriNode = settingValueNode.get(URI); - String secretID = null; - if (uriNode != null && !uriNode.isNull()) { - secretID = uriNode.asText(); // uri node contains the secret ID value - } - SecretReferenceConfigurationSetting secretReferenceConfigurationSetting = - new SecretReferenceConfigurationSetting(secretID, secretID) - .setKey(baseSetting.getKey()) + readSecretReferenceConfigurationSettingValue(baseSetting.getKey(), settingValue) .setValue(settingValue) .setLabel(baseSetting.getLabel()) .setETag(baseSetting.getETag()) @@ -199,7 +196,34 @@ private static ConfigurationSetting configurati return setting; } - private static FeatureFlagConfigurationSetting readFeatureFlagConfigurationSettingValue(String settingValue) { + /** + * Given a JSON format string {@code settingValue}, deserializes it into a {@link JsonNode} and returns a + * {@link SecretReferenceConfigurationSetting} object. + * + * @param key the {@code key} property of setting. + * @param settingValue a JSON format string that represents the {@code value} property of setting. + * @return A {@link SecretReferenceConfigurationSetting} object. + */ + public static SecretReferenceConfigurationSetting readSecretReferenceConfigurationSettingValue(String key, + String settingValue) { + final JsonNode settingValueNode = toJsonNode(settingValue); + + final JsonNode uriNode = settingValueNode.get(URI); + String secretID = null; + if (uriNode != null && !uriNode.isNull()) { + secretID = uriNode.asText(); // uri node contains the secret ID value + } + return new SecretReferenceConfigurationSetting(key, secretID); + } + + /** + * Given a JSON format string {@code settingValue}, deserializes it into a {@link JsonNode} and returns a + * {@link FeatureFlagConfigurationSetting} object. + * + * @param settingValue a JSON format string that represents the {@code value} property of setting. + * @return A {@link FeatureFlagConfigurationSetting} object which converted from the {@code settingValue}. + */ + public static FeatureFlagConfigurationSetting readFeatureFlagConfigurationSettingValue(String settingValue) { JsonNode settingValueNode = toJsonNode(settingValue); final JsonNode featureIdNode = settingValueNode.get(ID); diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/ConfigurationSettingJsonSerializer.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/ConfigurationSettingJsonSerializer.java index b50d10b082498..d2ca739416149 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/ConfigurationSettingJsonSerializer.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/ConfigurationSettingJsonSerializer.java @@ -51,6 +51,12 @@ public final class ConfigurationSettingJsonSerializer extends JsonSerializer properties, JsonGenerator gen) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/models/FeatureFlagConfigurationSetting.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/models/FeatureFlagConfigurationSetting.java index d5102ba3a2466..219645f2ef7ed 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/models/FeatureFlagConfigurationSetting.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/models/FeatureFlagConfigurationSetting.java @@ -3,23 +3,31 @@ package com.azure.data.appconfiguration.models; -import java.util.Collections; +import com.azure.core.util.logging.ClientLogger; + +import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static com.azure.data.appconfiguration.implementation.ConfigurationSettingJsonDeserializer.readFeatureFlagConfigurationSettingValue; +import static com.azure.data.appconfiguration.implementation.ConfigurationSettingJsonSerializer.writeFeatureFlagConfigurationSetting; /** * {@link FeatureFlagConfigurationSetting} allows you to customize your own feature flags to dynamically administer a * feature's lifecycle. Feature flags can be used to enable or disable features. */ public final class FeatureFlagConfigurationSetting extends ConfigurationSetting { - private final String featureId; - private final boolean isEnabled; + private static final ClientLogger LOGGER = new ClientLogger(FeatureFlagConfigurationSetting.class); + private static final String FEATURE_FLAG_CONTENT_TYPE = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"; + + private String featureId; + private boolean isEnabled; private String description; private String displayName; private List clientFilters; - private static final String FEATURE_FLAG_CONTENT_TYPE = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"; - /** * A prefix is used to construct a feature flag configuration setting's key. */ @@ -58,10 +66,24 @@ public FeatureFlagConfigurationSetting setKey(String key) { * @param value The value to associate with this configuration setting. * * @return The updated {@link FeatureFlagConfigurationSetting} object. + * @throws IllegalArgumentException if the setting's {@code value} is an invalid JSON format. */ @Override public FeatureFlagConfigurationSetting setValue(String value) { super.setValue(value); + // update strongly-typed properties. + final FeatureFlagConfigurationSetting updatedSetting = readFeatureFlagConfigurationSettingValue(value); + this.featureId = updatedSetting.getFeatureId(); + this.description = updatedSetting.getDescription(); + this.isEnabled = updatedSetting.isEnabled(); + this.displayName = updatedSetting.getDisplayName(); + if (updatedSetting.getClientFilters() != null) { + this.clientFilters = StreamSupport.stream(updatedSetting.getClientFilters().spliterator(), false) + .collect(Collectors.toList()); + } else { + this.clientFilters = null; + } + return this; } @@ -127,6 +149,21 @@ public String getFeatureId() { return featureId; } + /** + * Set the feature ID of this configuration setting. + * + * @param featureId the feature ID of this configuration setting. + * + * @return The updated {@link FeatureFlagConfigurationSetting} object. + * @throws IllegalArgumentException if the setting's {@code value} is an invalid JSON format. + */ + public FeatureFlagConfigurationSetting setFeatureId(String featureId) { + this.featureId = featureId; + super.setKey(KEY_PREFIX + featureId); + updateSettingValue(); + return this; + } + /** * Get the boolean indicator to show if the setting is turn on or off. * @@ -136,6 +173,20 @@ public boolean isEnabled() { return this.isEnabled; } + /** + * Set the boolean indicator to show if the setting is turn on or off. + * + * @param isEnabled the boolean indicator to show if the setting is turn on or off. + + * @return The updated {@link FeatureFlagConfigurationSetting} object. + * @throws IllegalArgumentException if the setting's {@code value} is an invalid JSON format. + */ + public FeatureFlagConfigurationSetting setEnabled(boolean isEnabled) { + this.isEnabled = isEnabled; + updateSettingValue(); + return this; + } + /** * Get the description of this configuration setting. * @@ -151,9 +202,11 @@ public String getDescription() { * @param description the description of this configuration setting. * * @return The updated {@link FeatureFlagConfigurationSetting} object. + * @throws IllegalArgumentException if the setting's {@code value} is an invalid JSON format. */ public FeatureFlagConfigurationSetting setDescription(String description) { this.description = description; + updateSettingValue(); return this; } @@ -172,9 +225,11 @@ public String getDisplayName() { * @param displayName the display name of this configuration setting. * * @return The updated {@link FeatureFlagConfigurationSetting} object. + * @throws IllegalArgumentException if the setting's {@code value} is an invalid JSON format. */ public FeatureFlagConfigurationSetting setDisplayName(String displayName) { this.displayName = displayName; + updateSettingValue(); return this; } @@ -183,8 +238,8 @@ public FeatureFlagConfigurationSetting setDisplayName(String displayName) { * * @return the feature flag filters of this configuration setting. */ - public Iterable getClientFilters() { - return Collections.unmodifiableList(clientFilters); + public List getClientFilters() { + return clientFilters; } /** @@ -193,9 +248,11 @@ public Iterable getClientFilters() { * @param clientFilters the feature flag filters of this configuration setting. * * @return The updated {@link FeatureFlagConfigurationSetting} object. + * @throws IllegalArgumentException if the setting's {@code value} is an invalid JSON format. */ public FeatureFlagConfigurationSetting setClientFilters(List clientFilters) { this.clientFilters = clientFilters; + updateSettingValue(); return this; } @@ -208,6 +265,16 @@ public FeatureFlagConfigurationSetting setClientFilters(List */ public FeatureFlagConfigurationSetting addClientFilter(FeatureFlagFilter clientFilter) { clientFilters.add(clientFilter); + updateSettingValue(); return this; } + + private void updateSettingValue() { + try { + super.setValue(writeFeatureFlagConfigurationSetting(this)); + } catch (IOException exception) { + LOGGER.logExceptionAsError(new IllegalArgumentException( + "Can't parse Feature Flag configuration setting value.", exception)); + } + } } diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/models/SecretReferenceConfigurationSetting.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/models/SecretReferenceConfigurationSetting.java index ac2be3466fa6d..c794de453d5fe 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/models/SecretReferenceConfigurationSetting.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/models/SecretReferenceConfigurationSetting.java @@ -4,16 +4,23 @@ package com.azure.data.appconfiguration.models; import com.azure.core.annotation.Fluent; +import com.azure.core.util.logging.ClientLogger; +import java.io.IOException; import java.util.Map; +import static com.azure.data.appconfiguration.implementation.ConfigurationSettingJsonDeserializer.readSecretReferenceConfigurationSettingValue; +import static com.azure.data.appconfiguration.implementation.ConfigurationSettingJsonSerializer.writeSecretReferenceConfigurationSetting; + /** * {@link SecretReferenceConfigurationSetting} model. It represents a configuration setting that references as * KeyVault secret. */ @Fluent public final class SecretReferenceConfigurationSetting extends ConfigurationSetting { - private final String secretId; + private static final ClientLogger LOGGER = new ClientLogger(SecretReferenceConfigurationSetting.class); + + private String secretId; private static final String SECRET_REFERENCE_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"; @@ -39,6 +46,20 @@ public String getSecretId() { return secretId; } + /** + * Set the secret ID value of this configuration setting. + * + * @param secretId the secret ID value of this configuration setting. + * + * @return The updated {@link SecretReferenceConfigurationSetting} object. + * @throws IllegalArgumentException if the setting's {@code value} is an invalid JSON format. + */ + public SecretReferenceConfigurationSetting setSecretId(String secretId) { + this.secretId = secretId; + updateSettingValue(); + return this; + } + /** * Sets the key of this setting. * @@ -58,10 +79,15 @@ public SecretReferenceConfigurationSetting setKey(String key) { * @param value The value to associate with this configuration setting. * * @return The updated {@link SecretReferenceConfigurationSetting} object. + * @throws IllegalArgumentException if the setting's {@code value} is an invalid JSON format. */ @Override public SecretReferenceConfigurationSetting setValue(String value) { super.setValue(value); + // update strongly-typed properties. + final SecretReferenceConfigurationSetting updatedSetting = readSecretReferenceConfigurationSettingValue( + super.getKey(), value); + this.secretId = updatedSetting.getSecretId(); return this; } @@ -113,4 +139,13 @@ public SecretReferenceConfigurationSetting setTags(Map tags) { super.setTags(tags); return this; } + + private void updateSettingValue() { + try { + super.setValue(writeSecretReferenceConfigurationSetting(this)); + } catch (IOException exception) { + LOGGER.logExceptionAsError(new IllegalArgumentException( + "Can't parse Secret Reference configuration setting value.", exception)); + } + } } diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/FeatureFlagSettingUnitTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/FeatureFlagSettingUnitTest.java new file mode 100644 index 0000000000000..f3190fdce6a91 --- /dev/null +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/FeatureFlagSettingUnitTest.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.data.appconfiguration; + +import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; +import com.azure.data.appconfiguration.models.FeatureFlagFilter; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting.KEY_PREFIX; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class FeatureFlagSettingUnitTest { + // Original value + static final String NEW_KEY = "newKey"; + static final String DESCRIPTION_VALUE = "newDescription"; + static final String DISPLAY_NAME_VALUE = "newDisplayName"; + static final String FILTER_NAME = "Microsoft.Percentage"; + static final boolean IS_ENABLED = false; + // Updated value + static final String UPDATED_KEY = "updatedKey"; + static final String UPDATED_DESCRIPTION_VALUE = "updatedDescription"; + static final String UPDATED_DISPLAY_NAME_VALUE = "updatedDisplayName"; + static final boolean UPDATED_IS_ENABLED = true; + + String getFeatureFlagConfigurationSettingValue(String id, String description, String displayName, + boolean isEnabled) { + return String.format("{\"id\":\"%s\",\"description\":\"%s\",\"display_name\":\"%s\"," + + "\"enabled\":%s," + + "\"conditions\":{\"client_filters\":" + + "[{\"name\":\"Microsoft.Percentage\",\"parameters\":{\"Value\":\"30\"}}]" + + "}}", + id, description, displayName, isEnabled); + } + + @Test + public void accessingStronglyTypedPropertiesAfterSettingDifferentFeatureFlagJSON() { + // Create a new feature flag configuration setting, + final List featureFlagFilters = Arrays.asList( + getFlagFilter(FILTER_NAME, getFilterParameters())); + FeatureFlagConfigurationSetting setting = getFeatureFlagConfigurationSetting(NEW_KEY, DESCRIPTION_VALUE, + DISPLAY_NAME_VALUE, IS_ENABLED, featureFlagFilters); + String expectedNewSettingValue = getFeatureFlagConfigurationSettingValue(NEW_KEY, DESCRIPTION_VALUE, + DISPLAY_NAME_VALUE, IS_ENABLED); + assertEquals(expectedNewSettingValue, setting.getValue()); + + String expectedUpdatedSettingValue = getFeatureFlagConfigurationSettingValue(UPDATED_KEY, + UPDATED_DESCRIPTION_VALUE, UPDATED_DISPLAY_NAME_VALUE, UPDATED_IS_ENABLED); + // Set the Value to some pre-populated + setting.setValue(expectedUpdatedSettingValue); + // Access strongly-typed property values + assertEquals(expectedUpdatedSettingValue, setting.getValue()); + assertEquals(UPDATED_KEY, setting.getFeatureId()); + assertEquals(UPDATED_DISPLAY_NAME_VALUE, setting.getDisplayName()); + assertEquals(UPDATED_DESCRIPTION_VALUE, setting.getDescription()); + assertEquals(UPDATED_IS_ENABLED, setting.isEnabled()); + } + + @Test + public void accessingValueAfterChangingStronglyTypedProperties() { + // Create a new feature flag configuration setting, + final List featureFlagFilters = Arrays.asList( + getFlagFilter(FILTER_NAME, getFilterParameters())); + FeatureFlagConfigurationSetting setting = getFeatureFlagConfigurationSetting(NEW_KEY, DESCRIPTION_VALUE, + DISPLAY_NAME_VALUE, IS_ENABLED, featureFlagFilters); + + String expectedNewSettingValue = getFeatureFlagConfigurationSettingValue(NEW_KEY, DESCRIPTION_VALUE, + DISPLAY_NAME_VALUE, IS_ENABLED); + assertEquals(expectedNewSettingValue, setting.getValue()); + // Change strongly-type properties. + setting.setFeatureId(UPDATED_KEY); + setting.setDescription(UPDATED_DESCRIPTION_VALUE); + setting.setDisplayName(UPDATED_DISPLAY_NAME_VALUE); + setting.setEnabled(UPDATED_IS_ENABLED); + + String expectedUpdatedSettingValue = getFeatureFlagConfigurationSettingValue(UPDATED_KEY, + UPDATED_DESCRIPTION_VALUE, UPDATED_DISPLAY_NAME_VALUE, UPDATED_IS_ENABLED); + // make sure the value reflect to the changes + assertEquals(KEY_PREFIX + UPDATED_KEY, setting.getKey()); + assertEquals(expectedUpdatedSettingValue, setting.getValue()); + } + + @Test + public void throwExceptionWhenInvalidNonJsonFeatureFlagValue() { + // Create a new feature flag configuration setting, + final List featureFlagFilters = Arrays.asList( + getFlagFilter(FILTER_NAME, getFilterParameters())); + FeatureFlagConfigurationSetting setting = getFeatureFlagConfigurationSetting(NEW_KEY, DESCRIPTION_VALUE, + DISPLAY_NAME_VALUE, IS_ENABLED, featureFlagFilters); + + // Throws IllegalStateException when setting value to non-JSON + assertThrows(IllegalStateException.class, () -> setting.setValue("Hello World")); + } + + private FeatureFlagConfigurationSetting getFeatureFlagConfigurationSetting(String id, String description, + String displayName, boolean isEnabled, List filters) { + return new FeatureFlagConfigurationSetting(id, isEnabled) + .setDescription(description) + .setDisplayName(displayName) + .setClientFilters(filters); + } + + private FeatureFlagFilter getFlagFilter(String filterName, Map filterParameters) { + return new FeatureFlagFilter(filterName).setParameters(filterParameters); + } + + private Map getFilterParameters() { + Map parameters = new HashMap<>(); + parameters.put("Value", "30"); + return parameters; + } +} diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/SecretReferenceConfigurationSettingUnitTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/SecretReferenceConfigurationSettingUnitTest.java new file mode 100644 index 0000000000000..edf00eece9b2e --- /dev/null +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/SecretReferenceConfigurationSettingUnitTest.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.data.appconfiguration; + +import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SecretReferenceConfigurationSettingUnitTest { + // Original value + static final String NEW_KEY = "newKey"; + static final String SECRET_ID_VALUE = "https://www.google.com/"; + // Updated value + static final String UPDATED_SECRET_ID_VALUE = "https://www.google.com/updated"; + @Test + public void accessingStronglyTypedPropertiesAfterSettingDifferentSecretReferenceJSON() { + // Create a new configuration setting, + SecretReferenceConfigurationSetting setting = getSecretReferenceConfigurationSetting(NEW_KEY, SECRET_ID_VALUE); + String expectedNewSettingValue = getSecretReferenceConfigurationSettingValue(SECRET_ID_VALUE); + assertEquals(expectedNewSettingValue, setting.getValue()); + + String expectedUpdatedSettingValue = getSecretReferenceConfigurationSettingValue(UPDATED_SECRET_ID_VALUE); + // Set the Value to some pre-populated + setting.setValue(expectedUpdatedSettingValue); + // Access strongly-typed property values + assertEquals(expectedUpdatedSettingValue, setting.getValue()); + assertEquals(UPDATED_SECRET_ID_VALUE, setting.getSecretId()); + } + + @Test + public void accessingValueAfterChangingStronglyTypedProperties() { + // Create a new feature flag configuration setting, + SecretReferenceConfigurationSetting setting = getSecretReferenceConfigurationSetting(NEW_KEY, SECRET_ID_VALUE); + String expectedNewSettingValue = getSecretReferenceConfigurationSettingValue(SECRET_ID_VALUE); + assertEquals(expectedNewSettingValue, setting.getValue()); + // Change strongly-type properties. + setting.setSecretId(UPDATED_SECRET_ID_VALUE); + String expectedUpdatedSettingValue = getSecretReferenceConfigurationSettingValue(UPDATED_SECRET_ID_VALUE); + // make sure the value reflect to the changes + assertEquals(expectedUpdatedSettingValue, setting.getValue()); + } + + @Test + public void throwExceptionWhenInvalidNonJsonSecretReferenceValue() { + // Create a new feature flag configuration setting, + SecretReferenceConfigurationSetting setting = getSecretReferenceConfigurationSetting(NEW_KEY, SECRET_ID_VALUE); + // Throws IllegalStateException when setting value to non-JSON + assertThrows(IllegalStateException.class, () -> setting.setValue("Hello World")); + } + + String getSecretReferenceConfigurationSettingValue(String secretId) { + return String.format("{\"uri\":\"%s\"}", secretId); + } + + private SecretReferenceConfigurationSetting getSecretReferenceConfigurationSetting(String key, String secretId) { + return new SecretReferenceConfigurationSetting(key, secretId); + } +}