All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.azure.spring.cloud.appconfiguration.config.implementation.FeatureFlagClient Maven / Gradle / Ivy

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation;

import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.CONDITIONS;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEFAULT_REQUIREMENT_TYPE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.E_TAG;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_ID;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_REFERENCE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.REQUIREMENT_TYPE_SERVICE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.SELECT_ALL_FEATURE_FLAGS;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.TELEMETRY;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.bouncycastle.jcajce.provider.digest.SHA256;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Feature;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.FeatureTelemetry;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.nimbusds.jose.util.Base64URL;

/**
 * Loads sets of feature flags, and de-duplicates the results with previously loaded feature flags. Newer Feature Flags
 * take priority.
 */
@Component
public class FeatureFlagClient {

    protected final Map properties = new LinkedHashMap<>();

    private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder()
        .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();

    /**
     * 

* Gets settings from Azure/Cache to set as configurations. Updates the cache. *

* *

* Note: Doesn't update Feature Management, just stores values in cache. Call {@code initFeatures} to update * Feature Management, but make sure its done in the last {@code AppConfigurationPropertySource} * AppConfigurationPropertySource} *

* */ public List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter, String[] labelFilter, boolean isRefresh) { List loadedFeatureFlags = new ArrayList<>(); String keyFilter = SELECT_ALL_FEATURE_FLAGS; if (StringUtils.hasText(customKeyFilter)) { keyFilter = FEATURE_FLAG_PREFIX + customKeyFilter; } List labels = Arrays.asList(labelFilter); Collections.reverse(labels); for (String label : labels) { SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter).setLabelFilter(label); FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, isRefresh); loadedFeatureFlags.addAll(proccessFeatureFlags(features, keyFilter)); } return loadedFeatureFlags; } public List proccessFeatureFlags(FeatureFlags features, String endpoint) { List loadedFeatureFlags = new ArrayList<>(); loadedFeatureFlags.add(features); // Reading In Features for (ConfigurationSetting setting : features.getFeatureFlags()) { if (setting instanceof FeatureFlagConfigurationSetting && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) { FeatureFlagConfigurationSetting featureFlag = (FeatureFlagConfigurationSetting) setting; properties.put(featureFlag.getKey(), createFeature(featureFlag, endpoint)); } } return loadedFeatureFlags; } /** * Creates a {@code Feature} from a {@code KeyValueItem} * * @param item Used to create Features before being converted to be set into properties. * @return Feature created from KeyValueItem */ protected static Feature createFeature(FeatureFlagConfigurationSetting item, String originEndpoint) { String requirementType = DEFAULT_REQUIREMENT_TYPE; FeatureTelemetry featureTelemetry = new FeatureTelemetry(); try { JsonNode node = CASE_INSENSITIVE_MAPPER.readTree(item.getValue()); JsonNode conditions = node.get(CONDITIONS); if (conditions != null && conditions.get(REQUIREMENT_TYPE_SERVICE) != null) { requirementType = conditions.get(REQUIREMENT_TYPE_SERVICE).asText(); } JsonNode telemetryNode = node.get(TELEMETRY); if (telemetryNode != null) { ObjectMapper objectMapper = JsonMapper.builder() .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); featureTelemetry = objectMapper.convertValue(telemetryNode, FeatureTelemetry.class); } } catch (JsonProcessingException e) { } Feature feature = new Feature(item, requirementType, featureTelemetry); if (feature.getTelemetry() != null) { final FeatureTelemetry telemetry = feature.getTelemetry(); if (telemetry.isEnabled()) { final Map originMetadata = telemetry.getMetadata(); originMetadata.put(FEATURE_FLAG_ID, calculateFeatureFlagId(item.getKey(), item.getLabel())); originMetadata.put(E_TAG, item.getETag()); if (originEndpoint != null && !originEndpoint.isEmpty()) { final String labelPart = item.getLabel().isEmpty() ? "" : String.format("?label=%s", item.getLabel()); originMetadata.put(FEATURE_FLAG_REFERENCE, String.format("%s/kv/%s%s", originEndpoint, item.getKey(), labelPart)); } } } return feature; } /** * @param key the key of feature flag * @param label the label of feature flag. If label is whitespace, treat as null * @return base64_url(SHA256(utf8_bytes("${key}\n${label}"))).replace('+', '-').replace('/', '_').trimEnd('=') * trimEnd() means trims everything after the first occurrence of the '=' */ private static String calculateFeatureFlagId(String key, String label) { final String data = String.format("%s\n%s", key, label.isEmpty() ? null : label); final SHA256.Digest digest = new SHA256.Digest(); final String beforeTrim = Base64URL.encode(digest.digest(data.getBytes(StandardCharsets.UTF_8))) .toString().replace('+', '-').replace('/', '_'); final int index = beforeTrim.indexOf('='); return beforeTrim.substring(0, index > -1 ? index : beforeTrim.length()); } /** * @return the properties */ public Map getProperties() { return properties; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy