updated data.sql and schema.sql with feature flag table

This commit is contained in:
XTiNCT 2026-02-08 13:17:13 +05:30
parent 871bccd2dc
commit c518db81a2
10 changed files with 537 additions and 433 deletions

View file

@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Custom annotation to mark methods protected by feature flags
* Custom annotation to mark methods protected by feature toggle
*
* Usage:
* @FeatureToggle(key = "add-new-pet")

View file

@ -1,116 +0,0 @@
package org.springframework.samples.petclinic.featureflag.aspect;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.samples.petclinic.featureflag.annotation.FeatureToggle;
import org.springframework.samples.petclinic.featureflag.service.FeatureFlagService;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* AOP Aspect that intercepts methods annotated with @FeatureToggle
* and checks if the feature is enabled before allowing execution
*/
@Aspect
@Component
public class FeatureFlagAspect {
private static final Logger logger = LoggerFactory.getLogger(FeatureFlagAspect.class);
private final FeatureFlagService featureFlagService;
public FeatureFlagAspect(FeatureFlagService featureFlagService) {
this.featureFlagService = featureFlagService;
}
@Around("@annotation(org.springframework.samples.petclinic.featureflag.annotation.FeatureToggle)")
public Object checkFeatureFlag(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
FeatureToggle featureToggle = method.getAnnotation(FeatureToggle.class);
String flagKey = featureToggle.key();
String contextParam = featureToggle.contextParam();
// Extract context if specified
String context = null;
if (!contextParam.isEmpty()) {
context = extractContextFromParams(method, joinPoint.getArgs(), contextParam);
}
// Check if feature is enabled
boolean isEnabled = featureFlagService.isFeatureEnabled(flagKey, context);
logger.debug("Feature flag '{}' check: enabled={}, context={}", flagKey, isEnabled, context);
if (isEnabled) {
// Feature is enabled, proceed with method execution
return joinPoint.proceed();
} else {
// Feature is disabled, handle based on configuration
logger.info("Feature '{}' is disabled, blocking execution", flagKey);
return handleDisabledFeature(joinPoint, featureToggle);
}
}
/**
* Extract context value from method parameters
*/
private String extractContextFromParams(Method method, Object[] args, String paramName) {
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
// Check if parameter name matches (requires -parameters compiler flag)
if (parameters[i].getName().equals(paramName)) {
if (args[i] != null) {
return args[i].toString();
}
}
// Also check for @RequestParam, @PathVariable annotations
for (var annotation : parameters[i].getAnnotations()) {
String annotationStr = annotation.toString();
if (annotationStr.contains(paramName)) {
if (args[i] != null) {
return args[i].toString();
}
}
}
}
return null;
}
/**
* Handle disabled feature - redirect or return error view
*/
private Object handleDisabledFeature(ProceedingJoinPoint joinPoint, FeatureToggle featureToggle) {
String redirect = featureToggle.disabledRedirect();
String message = featureToggle.disabledMessage();
// Look for RedirectAttributes in method parameters
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof RedirectAttributes) {
RedirectAttributes redirectAttrs = (RedirectAttributes) arg;
redirectAttrs.addFlashAttribute("message", message);
break;
}
}
// If redirect is specified, return redirect
if (!redirect.isEmpty()) {
return "redirect:" + redirect;
}
// Otherwise return to a generic disabled feature page or home
return "redirect:/oups"; // PetClinic error page
}
}

View file

@ -0,0 +1,117 @@
package org.springframework.samples.petclinic.featureflag.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.samples.petclinic.featureflag.annotation.FeatureToggle;
import org.springframework.samples.petclinic.featureflag.service.FeatureFlagService;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
/**
* AOP Aspect that intercepts methods annotated with @FeatureToggle and checks if the
* feature is enabled before allowing execution
*/
@Aspect
@Component
public class FeatureToggleAspect {
private static final Logger logger = LoggerFactory.getLogger(FeatureToggleAspect.class);
private final FeatureFlagService featureFlagService;
public FeatureToggleAspect(FeatureFlagService featureFlagService) {
this.featureFlagService = featureFlagService;
}
@Around("@annotation(org.springframework.samples.petclinic.featureflag.annotation.FeatureToggle)")
public Object checkFeatureToggle(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
FeatureToggle featureToggle = method.getAnnotation(FeatureToggle.class);
String flagKey = featureToggle.key();
String contextParam = featureToggle.contextParam();
// Extract context if specified
String context = null;
if (!contextParam.isEmpty()) {
context = extractContextFromParams(method, joinPoint.getArgs(), contextParam);
}
// Check if feature is enabled
boolean isEnabled = featureFlagService.isFeatureEnabled(flagKey, context);
logger.debug("Feature toggle '{}' check: enabled={}, context={}", flagKey, isEnabled, context);
if (isEnabled) {
// Feature is enabled, proceed with method execution
return joinPoint.proceed();
}
else {
// Feature is disabled, handle based on configuration
logger.info("Feature '{}' is disabled, blocking execution", flagKey);
return handleDisabledFeature(joinPoint, featureToggle);
}
}
/**
* Extract context value from method parameters
*/
private String extractContextFromParams(Method method, Object[] args, String paramName) {
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
// Check if parameter name matches (requires -parameters compiler flag)
if (parameters[i].getName().equals(paramName)) {
if (args[i] != null) {
return args[i].toString();
}
}
// Also check for @RequestParam, @PathVariable annotations
for (var annotation : parameters[i].getAnnotations()) {
String annotationStr = annotation.toString();
if (annotationStr.contains(paramName)) {
if (args[i] != null) {
return args[i].toString();
}
}
}
}
return null;
}
/**
* Handle disabled feature - redirect or return error view
*/
private Object handleDisabledFeature(ProceedingJoinPoint joinPoint, FeatureToggle featureToggle) {
String redirect = featureToggle.disabledRedirect();
String message = featureToggle.disabledMessage();
// Look for RedirectAttributes in method parameters
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof RedirectAttributes redirectAttrs) {
redirectAttrs.addFlashAttribute("message", message);
break;
}
}
// If redirect is specified, return redirect
if (!redirect.isEmpty()) {
return "redirect:" + redirect;
}
// Otherwise return to a generic disabled feature page or home
return "redirect:/oups"; // PetClinic error page
}
}

View file

@ -3,6 +3,7 @@ package org.springframework.samples.petclinic.featureflag.dto;
public class FeatureCheckRequest {
private String flagKey;
private String context;
public String getFlagKey() {

View file

@ -2,7 +2,9 @@ package org.springframework.samples.petclinic.featureflag.dto;
public class FeatureCheckResponse {
private String flagKey;
private Boolean enabled;
private String context;
public FeatureCheckResponse(String flagKey, Boolean enabled, String context) {

View file

@ -7,71 +7,89 @@ import org.springframework.samples.petclinic.featureflag.entity.FlagType;
public class FeatureFlagRequest {
private String flagKey;
private String description;
private FlagType flagType;
private Boolean enabled;
private Integer percentage;
private Set<String> listItems;
private String flagKey;
// Getters and Setters
public String getFlagKey() {
return flagKey;
}
private String description;
public void setFlagKey(String flagKey) {
this.flagKey = flagKey;
}
private FlagType flagType;
public String getDescription() {
return description;
}
private Boolean enabled;
public void setDescription(String description) {
this.description = description;
}
private Integer percentage;
public FlagType getFlagType() {
return flagType;
}
private Set<String> whitelist;
public void setFlagType(FlagType flagType) {
this.flagType = flagType;
}
private Set<String> blacklist;
public Boolean getEnabled() {
return enabled;
}
// Getters and Setters
public String getFlagKey() {
return flagKey;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public void setFlagKey(String flagKey) {
this.flagKey = flagKey;
}
public Integer getPercentage() {
return percentage;
}
public String getDescription() {
return description;
}
public void setPercentage(Integer percentage) {
this.percentage = percentage;
}
public void setDescription(String description) {
this.description = description;
}
public Set<String> getListItems() {
return listItems;
}
public FlagType getFlagType() {
return flagType;
}
public void setListItems(Set<String> listItems) {
this.listItems = listItems;
}
public void setFlagType(FlagType flagType) {
this.flagType = flagType;
}
public FeatureFlag toEntity() {
FeatureFlag flag = new FeatureFlag();
flag.setFlagKey(this.flagKey);
flag.setDescription(this.description);
flag.setFlagType(this.flagType != null ? this.flagType : FlagType.SIMPLE);
flag.setEnabled(this.enabled != null ? this.enabled : false);
flag.setPercentage(this.percentage);
flag.setBlacklist(this.listItems != null ? this.listItems : Set.of());
flag.setWhitelist(this.listItems != null ? this.listItems : Set.of());
return flag;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Integer getPercentage() {
return percentage;
}
public void setPercentage(Integer percentage) {
this.percentage = percentage;
}
public Set<String> getWhitelist() {
return whitelist;
}
public void setWhitelist(Set<String> whitelist) {
this.whitelist = whitelist;
}
public Set<String> getBlacklist() {
return blacklist;
}
public void setBlacklist(Set<String> blacklist) {
this.blacklist = blacklist;
}
/**
* Convert DTO to Entity
*/
public FeatureFlag toEntity() {
FeatureFlag flag = new FeatureFlag();
flag.setFlagKey(this.flagKey);
flag.setDescription(this.description);
flag.setFlagType(this.flagType != null ? this.flagType : FlagType.SIMPLE);
flag.setEnabled(this.enabled != null ? this.enabled : false);
flag.setPercentage(this.percentage);
flag.setWhitelist(this.whitelist != null ? this.whitelist : Set.of());
flag.setBlacklist(this.blacklist != null ? this.blacklist : Set.of());
return flag;
}
}

View file

@ -7,111 +7,121 @@ import org.springframework.samples.petclinic.featureflag.entity.FlagType;
public class FeatureFlagResponse {
private Long id;
private String flagKey;
private String description;
private FlagType flagType;
private Boolean enabled;
private Integer percentage;
private Set<String> whitelist;
private Set<String> blacklist;
private String createdAt;
private String updatedAt;
private Long id;
public static FeatureFlagResponse fromEntity(FeatureFlag flag) {
FeatureFlagResponse response = new FeatureFlagResponse();
response.setId(flag.getId());
response.setFlagKey(flag.getFlagKey());
response.setDescription(flag.getDescription());
response.setFlagType(flag.getFlagType());
response.setEnabled(flag.isEnabled());
response.setPercentage(flag.getPercentage());
response.setWhiteList(flag.getWhitelist());
response.setBlackList(flag.getBlacklist());
response.setCreatedAt(flag.getCreatedAt().toString());
response.setUpdatedAt(flag.getUpdatedAt().toString());
return response;
}
private String flagKey;
// Getters and Setters
public Long getId() {
return id;
}
private String description;
public void setId(Long id) {
this.id = id;
}
private FlagType flagType;
public String getFlagKey() {
return flagKey;
}
private Boolean enabled;
public void setFlagKey(String flagKey) {
this.flagKey = flagKey;
}
private Integer percentage;
public String getDescription() {
return description;
}
private Set<String> whitelist;
public void setDescription(String description) {
this.description = description;
}
private Set<String> blacklist;
public FlagType getFlagType() {
return flagType;
}
private String createdAt;
public void setFlagType(FlagType flagType) {
this.flagType = flagType;
}
private String updatedAt;
public Boolean getEnabled() {
return enabled;
}
public static FeatureFlagResponse fromEntity(FeatureFlag flag) {
FeatureFlagResponse response = new FeatureFlagResponse();
response.setId(flag.getId());
response.setFlagKey(flag.getFlagKey());
response.setDescription(flag.getDescription());
response.setFlagType(flag.getFlagType());
response.setEnabled(flag.isEnabled());
response.setPercentage(flag.getPercentage());
response.setWhitelist(flag.getWhitelist());
response.setBlacklist(flag.getBlacklist());
response.setCreatedAt(flag.getCreatedAt().toString());
response.setUpdatedAt(flag.getUpdatedAt().toString());
return response;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
// Getters and Setters
public Long getId() {
return id;
}
public Integer getPercentage() {
return percentage;
}
public void setId(Long id) {
this.id = id;
}
public void setPercentage(Integer percentage) {
this.percentage = percentage;
}
public String getFlagKey() {
return flagKey;
}
public Set<String> getWhiteList() {
return whitelist;
}
public void setFlagKey(String flagKey) {
this.flagKey = flagKey;
}
public void setWhiteList(Set<String> listItems) {
this.whitelist = listItems;
}
public String getDescription() {
return description;
}
public Set<String> getBlackList() {
return blacklist;
}
public void setDescription(String description) {
this.description = description;
}
public void setBlackList(Set<String> listItems) {
this.blacklist = listItems;
}
public FlagType getFlagType() {
return flagType;
}
public String getCreatedAt() {
return createdAt;
}
public void setFlagType(FlagType flagType) {
this.flagType = flagType;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public Boolean getEnabled() {
return enabled;
}
public String getUpdatedAt() {
return updatedAt;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Integer getPercentage() {
return percentage;
}
public void setPercentage(Integer percentage) {
this.percentage = percentage;
}
public Set<String> getWhitelist() {
return whitelist;
}
public void setWhitelist(Set<String> whitelist) {
this.whitelist = whitelist;
}
public Set<String> getBlacklist() {
return blacklist;
}
public void setBlacklist(Set<String> blacklist) {
this.blacklist = blacklist;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
}

View file

@ -22,211 +22,217 @@ import org.springframework.transaction.annotation.Transactional;
@Transactional
public class FeatureFlagService {
private static final Logger logger = LoggerFactory.getLogger(FeatureFlagService.class);
private final FeatureFlagRepository repository;
private static final Logger logger = LoggerFactory.getLogger(FeatureFlagService.class);
public FeatureFlagService(FeatureFlagRepository repository) {
this.repository = repository;
}
private final FeatureFlagRepository repository;
/**
* Main helper function to check if a feature is enabled
* Can be called from anywhere in the application
*
* @param flagKey The unique identifier for the feature flag
* @return true if feature is enabled, false otherwise
*/
public boolean isFeatureEnabled(String flagKey) {
return isFeatureEnabled(flagKey, null);
}
public FeatureFlagService(FeatureFlagRepository repository) {
this.repository = repository;
}
/**
* Check if feature is enabled for a specific context/user
*
* @param flagKey The unique identifier for the feature flag
* @param context Context identifier (e.g., userId, sessionId, email)
* @return true if feature is enabled for this context, false otherwise
*/
public boolean isFeatureEnabled(String flagKey, String context) {
try {
Optional<FeatureFlag> flagOpt = repository.findByFlagKey(flagKey);
if (flagOpt.isEmpty()) {
logger.warn("Feature flag '{}' not found, defaulting to disabled", flagKey);
return false;
}
/**
* Main helper function to check if a feature is enabled
* Can be called from anywhere in the application
*
* @param flagKey The unique identifier for the feature flag
* @return true if feature is enabled, false otherwise
*/
public boolean isFeatureEnabled(String flagKey) {
return isFeatureEnabled(flagKey, null);
}
FeatureFlag flag = flagOpt.get();
return evaluateFlag(flag, context);
} catch (Exception e) {
logger.error("Error evaluating feature flag '{}': {}", flagKey, e.getMessage());
// Fail safe: return false on errors
return false;
}
}
/**
* Check if feature is enabled for a specific context/user
*
* @param flagKey The unique identifier for the feature flag
* @param context Context identifier (e.g., userId, sessionId, email)
* @return true if feature is enabled for this context, false otherwise
*/
public boolean isFeatureEnabled(String flagKey, String context) {
try {
Optional<FeatureFlag> flagOpt = repository.findByFlagKey(flagKey.toUpperCase());
/**
* Evaluate a feature flag based on its type and configuration
*/
private boolean evaluateFlag(FeatureFlag flag, String context) {
// GLOBAL_DISABLE always returns false
if (flag.getFlagType() == FlagType.GLOBAL_DISABLE) {
logger.debug("Flag '{}' is globally disabled", flag.getFlagKey());
return false;
}
if (flagOpt.isEmpty()) {
logger.warn("Feature flag '{}' not found, defaulting to disabled", flagKey);
return false;
}
// If not enabled, return false (except for specific override cases)
if (!flag.isEnabled()) {
logger.debug("Flag '{}' is disabled", flag.getFlagKey());
return false;
}
FeatureFlag flag = flagOpt.get();
return evaluateFlag(flag, context);
switch (flag.getFlagType()) {
case SIMPLE:
return true; // Simple on/off
}
catch (Exception e) {
logger.error("Error evaluating feature flag '{}': {}", flagKey, e.getMessage());
// Fail safe: return false on errors
return false;
}
}
case WHITELIST:
return evaluateWhitelist(flag, context);
/**
* Evaluate a feature flag based on its type and configuration
* Evaluation order (highest to lowest priority):
* 1. GLOBAL_DISABLE - always returns false
* 2. Blacklist - if context is blacklisted, return false
* 3. Enabled check - if not enabled, return false
* 4. Type-specific evaluation (WHITELIST, PERCENTAGE, SIMPLE)
*/
private boolean evaluateFlag(FeatureFlag flag, String context) {
// GLOBAL_DISABLE always returns false
if (flag.getFlagType() == FlagType.GLOBAL_DISABLE) {
logger.debug("Flag '{}' is globally disabled", flag.getFlagKey());
return false;
}
case BLACKLIST:
return evaluateBlacklist(flag, context);
// Check blacklist first (highest priority after GLOBAL_DISABLE)
if (context != null && !context.trim().isEmpty()) {
if (flag.getBlacklist().contains(context.trim())) {
logger.debug("Flag '{}' - context '{}' is blacklisted", flag.getFlagKey(), context);
return false;
}
}
case PERCENTAGE:
return evaluatePercentage(flag, context);
// If not enabled, return false (except for specific override cases)
if (!flag.isEnabled()) {
logger.debug("Flag '{}' is disabled", flag.getFlagKey());
return false;
}
default:
logger.warn("Unknown flag type for '{}': {}", flag.getFlagKey(), flag.getFlagType());
return false;
}
}
switch (flag.getFlagType()) {
case SIMPLE:
return true; // Simple on/off
/**
* Whitelist: Only allow if context is in the list
*/
private boolean evaluateWhitelist(FeatureFlag flag, String context) {
if (context == null || context.trim().isEmpty()) {
logger.debug("Whitelist flag '{}' requires context, got null/empty", flag.getFlagKey());
return false;
}
case WHITELIST:
return evaluateWhitelist(flag, context);
boolean inWhitelist = flag.getWhitelist().contains(context.trim());
logger.debug("Whitelist flag '{}' for context '{}': {}", flag.getFlagKey(), context, inWhitelist);
return inWhitelist;
}
case BLACKLIST:
// If we got here, context is not in blacklist and flag is enabled
return true;
/**
* Blacklist: Allow unless context is in the list
*/
private boolean evaluateBlacklist(FeatureFlag flag, String context) {
if (context == null || context.trim().isEmpty()) {
// No context means not blacklisted
return true;
}
case PERCENTAGE:
return evaluatePercentage(flag, context);
boolean inBlacklist = flag.getBlacklist().contains(context.trim());
logger.debug("Blacklist flag '{}' for context '{}': {}", flag.getFlagKey(), context, !inBlacklist);
return !inBlacklist;
}
default:
logger.warn("Unknown flag type for '{}': {}", flag.getFlagKey(), flag.getFlagType());
return false;
}
}
/**
* Percentage: Enable for X% of requests using consistent hashing
*/
private boolean evaluatePercentage(FeatureFlag flag, String context) {
if (flag.getPercentage() == null || flag.getPercentage() < 0 || flag.getPercentage() > 100) {
logger.warn("Invalid percentage for flag '{}': {}", flag.getFlagKey(), flag.getPercentage());
return false;
}
/**
* Whitelist: Only allow if context is in the whitelist
*/
private boolean evaluateWhitelist(FeatureFlag flag, String context) {
if (context == null || context.trim().isEmpty()) {
logger.debug("Whitelist flag '{}' requires context, got null/empty", flag.getFlagKey());
return false;
}
if (flag.getPercentage() == 0) {
return false;
}
boolean inWhitelist = flag.getWhitelist().contains(context.trim());
logger.debug("Whitelist flag '{}' for context '{}': {}", flag.getFlagKey(), context, inWhitelist);
return inWhitelist;
}
if (flag.getPercentage() == 100) {
return true;
}
/**
* Percentage: Enable for X% of requests using consistent hashing
*/
private boolean evaluatePercentage(FeatureFlag flag, String context) {
if (flag.getPercentage() == null || flag.getPercentage() < 0 || flag.getPercentage() > 100) {
logger.warn("Invalid percentage for flag '{}': {}", flag.getFlagKey(), flag.getPercentage());
return false;
}
// Use consistent hashing to ensure same context always gets same result
String hashInput = flag.getFlagKey() + (context != null ? context : "");
int hash = Math.abs(hashInput.hashCode());
int bucket = hash % 100;
boolean enabled = bucket < flag.getPercentage();
logger.debug("Percentage flag '{}' for context '{}': bucket={}, percentage={}, enabled={}",
flag.getFlagKey(), context, bucket, flag.getPercentage(), enabled);
return enabled;
}
if (flag.getPercentage() == 0) {
return false;
}
// CRUD Operations
if (flag.getPercentage() == 100) {
return true;
}
public List<FeatureFlag> getAllFlags() {
return repository.findAll();
}
// Use consistent hashing to ensure same context always gets same result
String hashInput = flag.getFlagKey() + (context != null ? context : "");
int hash = Math.abs(hashInput.hashCode());
int bucket = hash % 100;
public Optional<FeatureFlag> getFlagById(Long id) {
return repository.findById(id);
}
boolean enabled = bucket < flag.getPercentage();
logger.debug("Percentage flag '{}' for context '{}': bucket={}, percentage={}, enabled={}",
flag.getFlagKey(), context, bucket, flag.getPercentage(), enabled);
return enabled;
}
public Optional<FeatureFlag> getFlagByKey(String flagKey) {
return repository.findByFlagKey(flagKey);
}
// CRUD Operations
public FeatureFlag createFlag(FeatureFlag flag) {
if (repository.existsByFlagKey(flag.getFlagKey())) {
throw new IllegalArgumentException("Feature flag with key '" + flag.getFlagKey() + "' already exists");
}
validateFlag(flag);
return repository.save(flag);
}
public List<FeatureFlag> getAllFlags() {
return repository.findAll();
}
public FeatureFlag updateFlag(Long id, FeatureFlag updatedFlag) {
FeatureFlag existingFlag = repository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Feature flag not found with id: " + id));
public Optional<FeatureFlag> getFlagById(Long id) {
return repository.findById(id);
}
// Don't allow changing the key
updatedFlag.setFlagKey(existingFlag.getFlagKey());
validateFlag(updatedFlag);
return repository.save(updatedFlag);
}
public Optional<FeatureFlag> getFlagByKey(String flagKey) {
return repository.findByFlagKey(flagKey.toUpperCase());
}
public void deleteFlag(Long id) {
if (!repository.existsById(id)) {
throw new IllegalArgumentException("Feature flag not found with id: " + id);
}
repository.deleteById(id);
}
public FeatureFlag createFlag(FeatureFlag flag) {
if (repository.existsByFlagKey(flag.getFlagKey().toUpperCase())) {
throw new IllegalArgumentException("Feature flag with key '" + flag.getFlagKey() + "' already exists");
}
public FeatureFlag toggleFlag(String flagKey) {
FeatureFlag flag = repository.findByFlagKey(flagKey)
.orElseThrow(() -> new IllegalArgumentException("Feature flag not found: " + flagKey));
flag.setEnabled(!flag.isEnabled());
return repository.save(flag);
}
validateFlag(flag);
return repository.save(flag);
}
/**
* Validate flag configuration
*/
private void validateFlag(FeatureFlag flag) {
if (flag.getFlagKey() == null || flag.getFlagKey().trim().isEmpty()) {
throw new IllegalArgumentException("Flag key cannot be empty");
}
public FeatureFlag updateFlag(Long id, FeatureFlag updatedFlag) {
FeatureFlag existingFlag = repository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Feature flag not found with id: " + id));
if (flag.getFlagType() == FlagType.PERCENTAGE) {
if (flag.getPercentage() == null || flag.getPercentage() < 0 || flag.getPercentage() > 100) {
throw new IllegalArgumentException("Percentage must be between 0 and 100");
}
}
// Don't allow changing the key
updatedFlag.setFlagKey(existingFlag.getFlagKey());
if (flag.getFlagType() == FlagType.WHITELIST ||
flag.getFlagType() == FlagType.BLACKLIST) {
if (flag.getWhitelist() == null || flag.getBlacklist().isEmpty()) {
logger.warn("Flag '{}' is of type {} but has no list items",
flag.getFlagKey(), flag.getFlagType());
}
}
}
}
validateFlag(updatedFlag);
return repository.save(updatedFlag);
}
public void deleteFlag(Long id) {
if (!repository.existsById(id)) {
throw new IllegalArgumentException("Feature flag not found with id: " + id);
}
repository.deleteById(id);
}
public FeatureFlag toggleFlag(String flagKey) {
FeatureFlag flag = repository.findByFlagKey(flagKey.toUpperCase())
.orElseThrow(() -> new IllegalArgumentException("Feature flag not found: " + flagKey));
flag.setEnabled(!flag.isEnabled());
return repository.save(flag);
}
/**
* Validate flag configuration
*/
private void validateFlag(FeatureFlag flag) {
if (flag.getFlagKey() == null || flag.getFlagKey().trim().isEmpty()) {
throw new IllegalArgumentException("Flag key cannot be empty");
}
if (flag.getFlagType() == FlagType.PERCENTAGE) {
if (flag.getPercentage() == null || flag.getPercentage() < 0 || flag.getPercentage() > 100) {
throw new IllegalArgumentException("Percentage must be between 0 and 100");
}
}
if (flag.getFlagType() == FlagType.WHITELIST) {
if (flag.getWhitelist() == null || flag.getWhitelist().isEmpty()) {
logger.warn("Flag '{}' is of type WHITELIST but has no whitelist items", flag.getFlagKey());
}
}
if (flag.getFlagType() == FlagType.BLACKLIST) {
if (flag.getBlacklist() == null || flag.getBlacklist().isEmpty()) {
logger.warn("Flag '{}' is of type BLACKLIST but has no blacklist items", flag.getFlagKey());
}
}
}
}

View file

@ -51,3 +51,44 @@ INSERT IGNORE INTO visits VALUES (1, 7, '2010-03-04', 'rabies shot');
INSERT IGNORE INTO visits VALUES (2, 8, '2011-03-04', 'rabies shot');
INSERT IGNORE INTO visits VALUES (3, 8, '2009-06-04', 'neutered');
INSERT IGNORE INTO visits VALUES (4, 7, '2008-09-04', 'spayed');
-- Sample data for feature flags
-- 1. SIMPLE flag: Add New Pet (enabled by default)
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
VALUES ('ADD_NEW_PET', 'Controls whether users can add new pets to an owner', 'SIMPLE', TRUE, NULL, NOW(), NOW());
-- 2. SIMPLE flag: Add Visit (enabled by default)
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
VALUES ('ADD_VISIT', 'Controls whether users can add new visits for pets', 'SIMPLE', TRUE, NULL, NOW(), NOW());
-- 3. WHITELIST flag: Owner Search (only specific users can search)
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
VALUES ('OWNER_SEARCH', 'Controls who can search for owners', 'WHITELIST', TRUE, NULL, NOW(), NOW());
-- Add whitelist items for owner-search (example user contexts)
INSERT IGNORE INTO feature_flag_whitelist (feature_flag_id, whitelist)
SELECT id, 'admin' FROM feature_flags WHERE flag_key = 'OWNER_SEARCH';
INSERT IGNORE INTO feature_flag_whitelist (feature_flag_id, whitelist)
SELECT id, 'Ramprakash' FROM feature_flags WHERE flag_key = 'OWNER_SEARCH';
-- 4. PERCENTAGE flag: New UI Theme (gradually roll out to 50% of users)
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
VALUES ('NEW_UI_THEME', 'Gradually roll out new UI theme', 'PERCENTAGE', TRUE, 50, NOW(), NOW());
-- 5. BLACKLIST flag: Delete Owner (block specific users from deleting)
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
VALUES ('DELETE_OWNER', 'Controls who can delete owners', 'BLACKLIST', TRUE, NULL, NOW(), NOW());
-- Add blacklist items
INSERT IGNORE INTO feature_flag_blacklist (feature_flag_id, blacklist)
SELECT id, 'guest' FROM feature_flags WHERE flag_key = 'DELETE_OWNER';
INSERT IGNORE INTO feature_flag_blacklist (feature_flag_id, blacklist)
SELECT id, 'readonly_user' FROM feature_flags WHERE flag_key = 'DELETE_OWNER';
-- 6. GLOBAL_DISABLE flag: Emergency shutdown example
INSERT IGNORE INTO feature_flags (flag_key, description, flag_type, enabled, percentage, created_at, updated_at)
VALUES ('EMERGENCY_SHUTDOWN', 'Emergency feature kill switch', 'GLOBAL_DISABLE', FALSE, NULL, NOW(), NOW());

View file

@ -53,3 +53,28 @@ CREATE TABLE IF NOT EXISTS visits (
description VARCHAR(255),
FOREIGN KEY (pet_id) REFERENCES pets(id)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS feature_flags (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
flag_key VARCHAR(255) NOT NULL UNIQUE,
description VARCHAR(500) NOT NULL,
flag_type VARCHAR(50) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
percentage INT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
INDEX(flag_key)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS feature_flag_whitelist (
feature_flag_id BIGINT NOT NULL,
whitelist VARCHAR(255),
FOREIGN KEY (feature_flag_id) REFERENCES feature_flags(id) ON DELETE CASCADE
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS feature_flag_blacklist (
feature_flag_id BIGINT NOT NULL,
blacklist VARCHAR(255),
FOREIGN KEY (feature_flag_id) REFERENCES feature_flags(id) ON DELETE CASCADE
) engine=InnoDB;