Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add warnings for unsupported feature flags (#18672) #19049

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Function;

import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
Expand Down Expand Up @@ -86,6 +87,10 @@ public class FeatureFlags implements Serializable {

private ApplicationConfiguration configuration;

private boolean isPropertiesFileChecked = false;

private boolean isSystemPropertiesChecked = false;

/**
* Generate FeatureFlags with given lookup data.
*
Expand Down Expand Up @@ -189,6 +194,9 @@ public void loadProperties() {

File featureFlagFile = getFeatureFlagFile();
if (featureFlagFile == null || !featureFlagFile.exists()) {
// Check once if there are unsupported feature flags in the system properties
checkForUnsupportedSystemProperties();

// Disable all features if no file exists
for (Feature f : features) {
f.setEnabled(
Expand All @@ -213,12 +221,16 @@ void loadProperties(InputStream propertiesStream) {

if (propertiesStream != null) {
props.load(propertiesStream);
// Check once if there are unsupported feature flags in the file
checkForUnsupportedFileProperties(props);
}
// Check once if there are unsupported feature flags in the system properties
checkForUnsupportedSystemProperties();
for (Feature f : features) {
// Allow users to override a feature flag with a system property
String propertyValue = System.getProperty(
SYSTEM_PROPERTY_PREFIX + f.getId(),
props.getProperty(getPropertyName(f.getId())));
props.getProperty(getFilePropertyName(f.getId())));

f.setEnabled(Boolean.parseBoolean(propertyValue));
}
Expand All @@ -240,7 +252,7 @@ private void saveProperties() {
continue;
}
properties.append("# ").append(feature.getTitle()).append("\n");
properties.append(getPropertyName(feature.getId()))
properties.append(getFilePropertyName(feature.getId()))
.append("=true\n");
}
if (!featureFlagFile.getParentFile().exists()) {
Expand Down Expand Up @@ -294,10 +306,14 @@ private Optional<Feature> getFeature(String featureId) {
.findFirst();
}

private String getPropertyName(String featureId) {
private String getFilePropertyName(String featureId) {
return "com.vaadin.experimental." + featureId;
}

private String getSystemPropertyName(String featureId) {
return SYSTEM_PROPERTY_PREFIX + featureId;
}

/**
* Enables or disables the given feature.
*
Expand Down Expand Up @@ -344,11 +360,39 @@ private boolean isDevelopmentMode() {
public String getEnableHelperMessage(Feature feature) {
return feature.getTitle()
+ " is not enabled. Enable it in the debug window or by adding "
+ getPropertyName(feature.getId())
+ getFilePropertyName(feature.getId())
+ "=true to src/main/resources/" + PROPERTIES_FILENAME;
}

private Logger getLogger() {
return LoggerFactory.getLogger(FeatureFlags.class);
}

private void checkForUnsupportedFileProperties(Properties fileProps) {
if (!isPropertiesFileChecked) {
checkForUnsupportedFeatureFlags(fileProps, this::getFilePropertyName);
isPropertiesFileChecked = true;
}
}

private void checkForUnsupportedSystemProperties() {
if (!isSystemPropertiesChecked) {
// Initially, filter all system properties to the ones with our prefix
Properties filteredSystemProps = new Properties();
System.getProperties().entrySet().stream()
.filter(property -> property.getKey().toString().startsWith(SYSTEM_PROPERTY_PREFIX))
.forEach(property -> filteredSystemProps.put(property.getKey(), property.getValue()));
checkForUnsupportedFeatureFlags(filteredSystemProps, this::getSystemPropertyName);
isSystemPropertiesChecked = true;
}
}

private void checkForUnsupportedFeatureFlags(Properties props, Function<String, String> propertyWithPrefix) {
for (Object property : props.keySet()) {
if (features.stream()
.noneMatch(feature -> propertyWithPrefix.apply(feature.getId()).equals(property))) {
getLogger().warn("Unsupported feature flag is present: {}", property);
mshabarov marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.di.Lookup;
Expand Down Expand Up @@ -405,6 +407,75 @@ public void get_concurrentAccess_vaadinContextLock_noDeadlock()
latch.await(1, TimeUnit.SECONDS));
}

@Test
public void propertiesFileCheckedForUnsupportedFeatureFlags() throws IOException {
Logger mockedLogger = Mockito.mock(Logger.class);

try (MockedStatic<LoggerFactory> context = Mockito.mockStatic(LoggerFactory.class)) {
context.when(() -> LoggerFactory.getLogger(FeatureFlags.class))
.thenReturn(mockedLogger);

createFeatureFlagsFile(
"com.vaadin.experimental.unsupportedFeature=true\ncom.vaadin.experimental.exampleFeatureFlag=true\n");
featureFlags.loadProperties();

Mockito.verify(mockedLogger, Mockito.never())
.warn("Unsupported feature flag is present: {}", "com.vaadin.experimental.exampleFeatureFlag");
Mockito.verify(mockedLogger, Mockito.times(1))
.warn("Unsupported feature flag is present: {}", "com.vaadin.experimental.unsupportedFeature");
}
}

@Test
public void propertiesFileCheckForUnsupportedFeatureFlagsRanOnlyOnce() throws IOException {
Logger mockedLogger = Mockito.mock(Logger.class);

try (MockedStatic<LoggerFactory> context = Mockito.mockStatic(LoggerFactory.class)) {
context.when(() -> LoggerFactory.getLogger(FeatureFlags.class))
.thenReturn(mockedLogger);

createFeatureFlagsFile(
"com.vaadin.experimental.unsupportedFeature=true\n");
featureFlags.loadProperties();
featureFlags.loadProperties();

Mockito.verify(mockedLogger, Mockito.times(1))
.warn("Unsupported feature flag is present: {}", "com.vaadin.experimental.unsupportedFeature");
}
}

@Test
public void systemPropertiesCheckedForUnsupportedFeatureFlags() throws IOException {
Logger mockedLogger = Mockito.mock(Logger.class);
String exampleProperty = FeatureFlags.SYSTEM_PROPERTY_PREFIX + "exampleFeatureFlag";
String unsupportedProperty = FeatureFlags.SYSTEM_PROPERTY_PREFIX + "unsupportedFeature";
var previousValue = System.getProperty(exampleProperty);

try (MockedStatic<LoggerFactory> mockedFactory = Mockito.mockStatic(LoggerFactory.class)) {
mockedFactory.when(() -> LoggerFactory.getLogger(FeatureFlags.class))
.thenReturn(mockedLogger);

System.setProperty(exampleProperty, "true");
System.setProperty(unsupportedProperty, "true");
// resetting feature flags to manually retry check (because it was run in @Before block)
context.removeAttribute(FeatureFlags.FeatureFlagsWrapper.class);
featureFlags = FeatureFlags.get(context);

Mockito.verify(mockedLogger, Mockito.never())
.warn("Unsupported feature flag is present: {}", exampleProperty);
Mockito.verify(mockedLogger, Mockito.times(1))
.warn("Unsupported feature flag is present: {}", unsupportedProperty);
}
finally {
if (previousValue == null) {
System.clearProperty(exampleProperty);
} else {
System.setProperty(exampleProperty, previousValue);
}
System.clearProperty(unsupportedProperty);
}
}

private boolean hasUsageStatsEntry(String name) {
return UsageStatistics.getEntries()
.filter(entry -> entry.getName().equals(name)).findFirst()
Expand Down