Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.jackrabbit.oak.commons.properties.SystemPropertySupplier;
import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.slf4j.Logger;
Comment thread
reschke marked this conversation as resolved.
import org.slf4j.LoggerFactory;

/**
* A feature toggle to control new functionality. The default state of a feature
Expand All @@ -34,12 +37,20 @@
* involves registering a feature toggle on the {@link Whiteboard} and
* potentially comes with some overhead (e.g. when the whiteboard is based on
* OSGi). Therefore, client code should not create a new feature, check the
* state and then immediately release/close it again. Instead a feature should
* state and then immediately release/close it again. Instead, a feature should
* be acquired initially, checked at runtime whenever needed and finally
* released when the client component is destroyed.
* <p>
* The default state of {@code false} can be overridden by a system property
* using {@linkplain #newFeatureWithSystemPropertyDefault(String, Whiteboard)},
* in which case The property name is derived from the toggle name. This helps to quickly verify

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* in which case The property name is derived from the toggle name. This helps to quickly verify
* in which case the property name is derived from the toggle name. This helps to quickly verify

* a new feature. For a toggle named {@code "foo"}, the system property name is
* {@code oak-feature.foo}.
*/
public final class Feature implements Closeable {

private static final Logger LOG = LoggerFactory.getLogger(Feature.class);

private final AtomicBoolean value;

private final Registration registration;
Expand All @@ -49,6 +60,16 @@
this.registration = registration;
}

private static Feature internalNewFeatureWithSystemPropertyDefault(String name, Whiteboard whiteboard, boolean withSysPropDefault) {
// by default the initial value is false, but it can be overridden by a system property
AtomicBoolean value = withSysPropDefault ? new AtomicBoolean(
SystemPropertySupplier.create("oak-feature." + name, false).

Check failure on line 66 in oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/toggle/Feature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Fix this call that leads to an IllegalArgumentException.

See more on https://sonarcloud.io/project/issues?id=org.apache.jackrabbit%3Ajackrabbit-oak&issues=AZ67HNLChFEtutIvwN7R&open=AZ67HNLChFEtutIvwN7R&pullRequest=2947
loggingTo(LOG).get()) : new AtomicBoolean();
FeatureToggle adapter = new FeatureToggle(name, value);
return new Feature(value, whiteboard.register(
FeatureToggle.class, adapter, Collections.emptyMap()));
}

/**
* Creates a new {@link Feature} with the given name and registers the
* corresponding {@link FeatureToggle} on the {@link Whiteboard}.
Expand All @@ -60,10 +81,15 @@
* @return the feature toggle.
*/
public static Feature newFeature(String name, Whiteboard whiteboard) {
AtomicBoolean value = new AtomicBoolean();
FeatureToggle adapter = new FeatureToggle(name, value);
return new Feature(value, whiteboard.register(
FeatureToggle.class, adapter, Collections.emptyMap()));
return internalNewFeatureWithSystemPropertyDefault(name, whiteboard, false);
}

/**
* Same as {@linkplain #newFeature(String, Whiteboard)}, but with the initial state provided
* by a system property, named based on the feature's name.
*/
public static Feature newFeatureWithSystemPropertyDefault(String name, Whiteboard whiteboard) {
return internalNewFeatureWithSystemPropertyDefault(name, whiteboard, true);

Check failure on line 92 in oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/toggle/Feature.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Fix this call that leads to an IllegalArgumentException.

See more on https://sonarcloud.io/project/issues?id=org.apache.jackrabbit%3Ajackrabbit-oak&issues=AZ69TK3Omo_VJOIm9IgX&open=AZ69TK3Omo_VJOIm9IgX&pullRequest=2947
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
@Version("1.0.0")
@Version("1.1.0")
package org.apache.jackrabbit.oak.spi.toggle;

import org.osgi.annotation.versioning.Version;
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.junit.Test;

import static org.apache.jackrabbit.oak.spi.toggle.Feature.newFeature;
import static org.apache.jackrabbit.oak.spi.toggle.Feature.newFeatureWithSystemPropertyDefault;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.core.Is.is;
Expand All @@ -50,6 +51,28 @@ public void disabledByDefault() {
}
}

@Test

@rishabhdaim rishabhdaim Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional (for local consideration only — not pushing code): defaultOverriddenAsTrue / defaultOverriddenAsFalse already cover explicit boolean overrides. For other oak-feature.* values, SystemPropertySupplier uses Boolean.valueOf(String) semantics: only "true" (case-insensitive) enables the toggle; strings like "yes", "garbage", "1", etc. are treated as false, not as a parse error with log-and-fallback. If you want to document that contract in tests, something like:

    /**
     * SystemPropertySupplier uses {@link Boolean#valueOf(String)} for boolean props: only
     * {@code "true"} (case-insensitive) enables the toggle; values like {@code "yes"} or
     * {@code "garbage"} are treated as {@code false}, not as a parse error.
     */
    @Test
    public void defaultOverriddenWithNonTrueValueStaysDisabled() {
        String sysPropName = "oak-feature.my.toggle";
        for (String value : new String[] {"garbage", "yes", "1", "on", ""}) {
            System.setProperty(sysPropName, value);
            try (Feature feature = newFeatureWithSystemPropertyDefault("my.toggle", whiteboard)) {
                assertFalse("property value '" + value + "' must not enable the toggle", feature.isEnabled());
            } finally {
                System.clearProperty(sysPropName);
            }
        }
    }

public void defaultOverriddenAsTrue() {
String sysPropName = "oak-feature.my.toggle";
System.setProperty(sysPropName, "true");
try (Feature feature = newFeatureWithSystemPropertyDefault("my.toggle", whiteboard)) {
assertTrue(feature.isEnabled());
} finally {
System.clearProperty(sysPropName);
}
}

@Test
public void defaultOverriddenAsFalse() {
String sysPropName = "oak-feature.my.toggle";
System.setProperty(sysPropName, "false");
try (Feature feature = newFeatureWithSystemPropertyDefault("my.toggle", whiteboard)) {
assertFalse(feature.isEnabled());
} finally {
System.clearProperty(sysPropName);
}
}

@Test
public void register() {
try (Feature feature = newFeature("my.toggle", whiteboard)) {
Expand Down
1 change: 1 addition & 0 deletions oak-core-spi/src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@
<appender-ref ref="file"/>
</root>

<logger name="org.apache.jackrabbit.oak.spi.toggle.Feature" level="INFO"/>
</configuration>
Loading