Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package com.azure.spring.cloud.feature.management;

import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
Expand All @@ -17,7 +16,7 @@
import com.azure.spring.cloud.feature.management.filters.FeatureFilter;
import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties;
import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties;
import com.azure.spring.cloud.feature.management.implementation.models.Feature;
import com.azure.spring.cloud.feature.management.models.Feature;
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
import com.azure.spring.cloud.feature.management.models.FilterNotFoundException;

Expand Down Expand Up @@ -51,9 +50,9 @@ public class FeatureManager {
}

/**
* Checks to see if the feature is enabled. If enabled it check each filter, once a single filter
* returns true it returns true. If no filter returns true, it returns false. If there are no
* filters, it returns true. If feature isn't found it returns false.
* Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it
* returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature
* isn't found it returns false.
*
* @param feature Feature being checked.
* @return state of the feature
Expand All @@ -64,9 +63,9 @@ public Mono<Boolean> isEnabledAsync(String feature) {
}

/**
* Checks to see if the feature is enabled. If enabled it check each filter, once a single filter
* returns true it returns true. If no filter returns true, it returns false. If there are no
* filters, it returns true. If feature isn't found it returns false.
* Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it
* returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature
* isn't found it returns false.
*
* @param feature Feature being checked.
* @return state of the feature
Expand All @@ -76,34 +75,28 @@ public Boolean isEnabled(String feature) throws FilterNotFoundException {
return checkFeature(feature);
}

private boolean checkFeature(String feature) throws FilterNotFoundException {
if (featureManagementConfigurations.getFeatureManagement() == null
|| featureManagementConfigurations.getOnOff() == null) {
return false;
}

Boolean boolFeature = featureManagementConfigurations.getOnOff().get(feature);

if (boolFeature != null) {
return boolFeature;
}
private boolean checkFeature(String featureName) throws FilterNotFoundException {
Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream()
.filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null);

Feature featureItem = featureManagementConfigurations.getFeatureManagement().get(feature);

if (featureItem == null || !featureItem.getEvaluate()) {
if (featureFlag == null) {
return false;
}

Stream<FeatureFilterEvaluationContext> filters = featureItem.getEnabledFor().values().stream()
Stream<FeatureFilterEvaluationContext> filters = featureFlag.getConditions().getClientFilters().stream()
.filter(Objects::nonNull).filter(featureFilter -> featureFilter.getName() != null);

if (featureFlag.getConditions().getClientFilters().size() == 0) {
return featureFlag.isEnabled();
}

// All Filters must be true
if (featureItem.getRequirementType().equals("All")) {
return filters.allMatch(featureFilter -> isFeatureOn(featureFilter, feature));
if (featureFlag.getConditions().getRequirementType().equals("All")) {
return filters.allMatch(featureFilter -> isFeatureOn(featureFilter, featureName));
}

// Any Filter must be true
return filters.anyMatch(featureFilter -> isFeatureOn(featureFilter, feature));
return filters.anyMatch(featureFilter -> isFeatureOn(featureFilter, featureName));
}

private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String feature) {
Expand All @@ -129,25 +122,7 @@ private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String featur
* @return a set of all feature names
*/
public Set<String> getAllFeatureNames() {
Set<String> allFeatures = new HashSet<>();

allFeatures.addAll(featureManagementConfigurations.getOnOff().keySet());
allFeatures.addAll(featureManagementConfigurations.getFeatureManagement().keySet());
return allFeatures;
return new HashSet<String>(
featureManagementConfigurations.getFeatureFlags().stream().map(feature -> feature.getId()).toList());
}

/**
* @return the featureManagement
*/
Map<String, Feature> getFeatureManagement() {
return featureManagementConfigurations.getFeatureManagement();
}

/**
* @return the onOff
*/
Map<String, Boolean> getOnOff() {
return featureManagementConfigurations.getOnOff();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

package com.azure.spring.cloud.feature.management.implementation;

import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.Map;

import org.springframework.util.StringUtils;

public class FeatureFilterUtils {

/**
* Looks at the given key in the parameters and coverts it to a list if it is currently a map.
*
Expand All @@ -21,6 +22,8 @@ public static void updateValueFromMapToList(Map<String, Object> parameters, Stri
if (objectMap instanceof Map) {
Collection<Object> toType = ((Map<String, Object>) objectMap).values();
parameters.put(key, toType);
} else if (objectMap != null) {
parameters.put(key, objectMap);
}
}

Expand All @@ -30,4 +33,5 @@ public static String getKeyCase(Map<String, Object> parameters, String key) {
}
return StringUtils.uncapitalize(key);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,195 +2,28 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.implementation;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;

import com.azure.spring.cloud.feature.management.implementation.models.Feature;
import com.azure.spring.cloud.feature.management.implementation.models.ServerSideFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.azure.spring.cloud.feature.management.models.Feature;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Configuration Properties for Feature Management. Processes the configurations to be usable by Feature Management.
*/
@ConfigurationProperties(prefix = "feature-management")
public class FeatureManagementProperties extends HashMap<String, Object> {
public class FeatureManagementProperties {

private static final Logger LOGGER = LoggerFactory.getLogger(FeatureManagementProperties.class);
@JsonProperty("feature-flags")
private List<Feature> featureFlags;

private static final ObjectMapper MAPPER = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE);

private static final long serialVersionUID = -1642032123104805346L;

private static final String FEATURE_FLAG_SNAKE_CASE = "feature_flags";

/**
* Map of all Feature Flags that use Feature Filters.
*/
private transient Map<String, Feature> featureManagement;

/**
* Map of all Feature Flags that are just enabled/disabled.
*/
private Map<String, Boolean> onOff;

public FeatureManagementProperties() {
featureManagement = new HashMap<>();
onOff = new HashMap<>();
}

@Override
public void putAll(Map<? extends String, ? extends Object> m) {
if (m == null) {
return;
}
// Need to reset or switch between on/off to conditional doesn't work
featureManagement = new HashMap<>();
onOff = new HashMap<>();

// try to parse the properties by server side schema as default
tryServerSideSchema(m);

if (featureManagement.isEmpty() && onOff.isEmpty()) {
tryClientSideSchema(m);
}
}

@SuppressWarnings({"unchecked", "deprecation"})
private void tryServerSideSchema(Map<? extends String, ? extends Object> features) {
if (features.keySet().isEmpty()) {
return;
}

// check if FeatureFlags section exist
String featureFlagsSectionKey = "";
for (String key : features.keySet()) {
if (FEATURE_FLAG_SNAKE_CASE.equalsIgnoreCase(key)) {
featureFlagsSectionKey = key;
break;
}
}
if (featureFlagsSectionKey.isEmpty()) {
return;
}

// get FeatureFlags section and parse
final Object featureFlagsObject = features.get(featureFlagsSectionKey);
if (Map.class.isAssignableFrom(featureFlagsObject.getClass())) {
final Map<String, Object> featureFlagsSection = (Map<String, Object>) featureFlagsObject;
for (String key : featureFlagsSection.keySet()) {
addServerSideFeature(featureFlagsSection, key);
}
} else {
if (List.class.isAssignableFrom(featureFlagsObject.getClass())) {
final List<Object> featureFlagsSection = (List<Object>) featureFlagsObject;
for (Object flag : featureFlagsSection) {
addServerSideFeature((Map<? extends String, ?>) flag, null);
}
}
}
}

private void tryClientSideSchema(Map<? extends String, ? extends Object> features) {
for (String key : features.keySet()) {
addFeature(features, key, "");
}
}

@SuppressWarnings("unchecked")
private void addFeature(Map<? extends String, ? extends Object> features, String key, String combined) {
Object featureValue = features.get(key);
if (!combined.isEmpty() && !combined.endsWith(".")) {
combined += ".";
}
if (featureValue instanceof Boolean) {
onOff.put(combined + key, (Boolean) featureValue);
} else {
Feature feature = null;
try {
feature = MAPPER.convertValue(featureValue, Feature.class);
} catch (IllegalArgumentException e) {
LOGGER.error("Found invalid feature {} with value {}.", combined + key, featureValue.toString());
}
// When coming from a file "feature.flag" is not a possible flag name
if (feature != null && feature.getEnabledFor() == null && feature.getKey() == null) {
if (Map.class.isAssignableFrom(featureValue.getClass())) {
features = (Map<String, Object>) featureValue;
for (String fKey : features.keySet()) {
addFeature(features, fKey, combined + key);
}
}
} else {
if (feature != null) {
feature.setKey(key);
featureManagement.put(key, feature);
}
}
}
}

@SuppressWarnings("unchecked")
private void addServerSideFeature(Map<? extends String, ? extends Object> features, String key) {
Object featureValue = null;
if (key != null) {
featureValue = features.get(key);
} else {
featureValue = features;
}

ServerSideFeature serverSideFeature = null;
try {
LinkedHashMap<String, Object> ff = new LinkedHashMap<>();
if (featureValue.getClass().isAssignableFrom(LinkedHashMap.class)) {
ff = (LinkedHashMap<String, Object>) featureValue;
}
LinkedHashMap<String, Object> conditions = new LinkedHashMap<>();
if (ff.containsKey("conditions")
&& ff.get("conditions").getClass().isAssignableFrom(LinkedHashMap.class)) {
conditions = (LinkedHashMap<String, Object>) ff.get("conditions");
}
FeatureFilterUtils.updateValueFromMapToList(conditions, "client_filters");

serverSideFeature = MAPPER.convertValue(featureValue, ServerSideFeature.class);
} catch (IllegalArgumentException e) {
LOGGER.error("Found invalid feature {} with value {}.", key, featureValue.toString());
}

if (serverSideFeature != null && serverSideFeature.getId() != null) {
if (serverSideFeature.getConditions() != null
&& serverSideFeature.getConditions().getClientFilters() != null
&& serverSideFeature.getConditions().getClientFilters().size() > 0) {
final Feature feature = new Feature();
feature.setKey(serverSideFeature.getId());
feature.setEvaluate(serverSideFeature.isEnabled());
feature.setEnabledFor(serverSideFeature.getConditions().getClientFiltersAsMap());
feature.setRequirementType(serverSideFeature.getConditions().getRequirementType());
featureManagement.put(serverSideFeature.getId(), feature);
} else {
onOff.put(serverSideFeature.getId(), serverSideFeature.isEnabled());
}
}
}

/**
* @return the featureManagement
*/
public Map<String, Feature> getFeatureManagement() {
return featureManagement;
public List<Feature> getFeatureFlags() {
return featureFlags;
}

/**
* @return the onOff
*/
public Map<String, Boolean> getOnOff() {
return onOff;
public void setFeatureFlags(List<Feature> featureFlags) {
this.featureFlags = featureFlags;
}

}
Loading
Loading