SLING-11738 : Add option to prevent request override
diff --git a/.gitignore b/.gitignore
index 5b783ed..1ef01fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,5 +13,6 @@
*.bak
.vlt
.DS_Store
+.vscode
jcr.log
atlassian-ide-plugin.xml
diff --git a/bnd.bnd b/bnd.bnd
deleted file mode 100644
index ddf92e6..0000000
--- a/bnd.bnd
+++ /dev/null
@@ -1 +0,0 @@
-Require-Capability: osgi.implementation;filter:="(&(osgi.implementation=osgi.http)(version>=1.0)(!(version>=2.0)))"
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c793a29..19bd764 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>org.apache.sling</groupId>
<artifactId>sling-bundle-parent</artifactId>
- <version>47</version>
+ <version>49</version>
<relativePath />
</parent>
@@ -55,10 +55,19 @@
<dependency>
<groupId>org.osgi</groupId>
<artifactId>osgi.core</artifactId>
+ <version>8.0.0</version>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.component.annotations</artifactId>
+ <version>1.5.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component</artifactId>
+ <version>1.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -74,22 +83,37 @@
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.http.whiteboard</artifactId>
- <version>1.0.0</version>
+ <version>1.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.api</artifactId>
- <version>2.1.0</version>
+ <version>2.25.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
+ <version>3.1.0</version>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.13.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>4.10.0</version>
+ <scope>test</scope>
</dependency>
</dependencies>
</project>
diff --git a/src/main/java/org/apache/sling/featureflags/Features.java b/src/main/java/org/apache/sling/featureflags/Features.java
index e9b7f7d..93b67ec 100644
--- a/src/main/java/org/apache/sling/featureflags/Features.java
+++ b/src/main/java/org/apache/sling/featureflags/Features.java
@@ -18,6 +18,8 @@
*/
package org.apache.sling.featureflags;
+import java.util.Collection;
+
import org.osgi.annotation.versioning.ProviderType;
/**
@@ -35,11 +37,24 @@
* are configured with OSGi configuration whose factory PID is
* {@code org.apache.sling.featureflags.Feature}.
*
- * @return The known features
+ * @return The known features, the array might be empty
+ * @deprecated Use {@link #getAllFeatures()} instead
*/
Feature[] getFeatures();
/**
+ * Get the list of all (known) features.
+ * <p>
+ * Features are known if they are registered as {@link Feature} services or
+ * are configured with OSGi configuration whose factory PID is
+ * {@code org.apache.sling.featureflags.Feature}.
+ *
+ * @return The known features, the collection might be empty
+ * @since 1.2.0
+ */
+ Collection<Feature> getAllFeatures();
+
+ /**
* Returns the feature with the given name.
* <p>
* Features are known if they are registered as {@link Feature} services or
diff --git a/src/main/java/org/apache/sling/featureflags/impl/ConfiguredFeature.java b/src/main/java/org/apache/sling/featureflags/impl/ConfiguredFeature.java
index 7280f64..7b3565c 100644
--- a/src/main/java/org/apache/sling/featureflags/impl/ConfiguredFeature.java
+++ b/src/main/java/org/apache/sling/featureflags/impl/ConfiguredFeature.java
@@ -28,16 +28,14 @@
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Modified;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@Designate(ocd = ConfiguredFeature.Config.class, factory = true)
@Component(service = Feature.class,
- configurationPolicy = ConfigurationPolicy.REQUIRE,
- property = {
- Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
- })
+ configurationPolicy = ConfigurationPolicy.REQUIRE)
public class ConfiguredFeature implements Feature {
@ObjectClassDefinition(name = "Apache Sling Configured Feature",
@@ -59,18 +57,23 @@
@AttributeDefinition(name = "Enabled", description = "Boolean flag indicating whether the feature is "
+ "enabled or not by this configuration")
boolean enabled() default false;
+
+ @AttributeDefinition(name = "Allow Override", description = "If disabled, the enabled state can't be overriden for a request")
+ boolean allowOverride() default true;
}
private static final String PROP_FEATURE = "feature";
+ private volatile String name;
- private String name;
+ private volatile String description;
- private String description;
+ private volatile boolean enabled;
- private boolean enabled;
+ private volatile boolean allowOverride;
@Activate
+ @Modified
private void activate(final Config config, final Map<String, Object> properties) {
this.name = config.name();
if ( this.name == null ) {
@@ -87,24 +90,25 @@
this.description = this.name;
}
this.enabled = config.enabled();
+ this.allowOverride = config.allowOverride();
}
@Override
- public boolean isEnabled(ExecutionContext context) {
-
- // Request Parameter Override
- ServletRequest request = context.getRequest();
- if (request != null) {
- String[] features = request.getParameterValues(PROP_FEATURE);
- if (features != null) {
- for (String feature : features) {
- if (this.name.equals(feature)) {
- return true;
+ public boolean isEnabled(final ExecutionContext context) {
+ if ( this.allowOverride ) {
+ // Request Parameter Override
+ final ServletRequest request = context.getRequest();
+ if (request != null) {
+ final String[] features = request.getParameterValues(PROP_FEATURE);
+ if (features != null) {
+ for (String feature : features) {
+ if (this.name.equals(feature)) {
+ return true;
+ }
}
- }
+ }
}
}
-
return this.enabled;
}
diff --git a/src/main/java/org/apache/sling/featureflags/impl/ExecutionContextImpl.java b/src/main/java/org/apache/sling/featureflags/impl/ExecutionContextImpl.java
index 8a95302..877140d 100644
--- a/src/main/java/org/apache/sling/featureflags/impl/ExecutionContextImpl.java
+++ b/src/main/java/org/apache/sling/featureflags/impl/ExecutionContextImpl.java
@@ -36,30 +36,14 @@
private static final String REQUEST_ATTRIBUTE_RESOLVER = "org.apache.sling.auth.core.ResourceResolver";
- private final ResourceResolver resourceResolver;
-
private final HttpServletRequest request;
- private final Map<String, Boolean> featureCache;
+ private volatile Map<String, Boolean> featureCache;
private final Features features;
public ExecutionContextImpl(final Features features, final HttpServletRequest request) {
- ResourceResolver resourceResolver = null;
- if (request != null) {
- if ( request instanceof SlingHttpServletRequest ) {
- resourceResolver = ((SlingHttpServletRequest)request).getResourceResolver();
- } else {
- Object resolverObject = request.getAttribute(REQUEST_ATTRIBUTE_RESOLVER);
- if (resolverObject instanceof ResourceResolver) {
- resourceResolver = (ResourceResolver) resolverObject;
- }
- }
- }
-
this.request = request;
- this.resourceResolver = resourceResolver;
- this.featureCache = new HashMap<String, Boolean>();
this.features = features;
}
@@ -70,7 +54,18 @@
@Override
public ResourceResolver getResourceResolver() {
- return this.resourceResolver;
+ ResourceResolver resourceResolver = null;
+ if (request != null) {
+ if ( request instanceof SlingHttpServletRequest ) {
+ resourceResolver = ((SlingHttpServletRequest)request).getResourceResolver();
+ } else {
+ final Object resolverObject = request.getAttribute(REQUEST_ATTRIBUTE_RESOLVER);
+ if (resolverObject instanceof ResourceResolver) {
+ resourceResolver = (ResourceResolver) resolverObject;
+ }
+ }
+ }
+ return resourceResolver;
}
@Override
@@ -79,14 +74,17 @@
}
boolean isEnabled(final Feature feature) {
+ if ( this.featureCache == null ) {
+ this.featureCache = new HashMap<>();
+ }
final String name = feature.getName();
- Boolean entry = this.featureCache.get(name);
- if (entry == null) {
+ Boolean result = this.featureCache.get(name);
+ if (result == null) {
// put false in the cache to stop on circular calls
this.featureCache.put(name, Boolean.FALSE);
- entry = feature.isEnabled(this);
- this.featureCache.put(name, entry);
+ result = feature.isEnabled(this);
+ this.featureCache.put(name, result);
}
- return entry;
+ return result;
}
}
diff --git a/src/main/java/org/apache/sling/featureflags/impl/FeatureManager.java b/src/main/java/org/apache/sling/featureflags/impl/FeatureManager.java
index 17464fd..b225341 100644
--- a/src/main/java/org/apache/sling/featureflags/impl/FeatureManager.java
+++ b/src/main/java/org/apache/sling/featureflags/impl/FeatureManager.java
@@ -21,16 +21,18 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
+import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
-import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -46,9 +48,10 @@
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.propertytypes.ServiceRanking;
import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardContextSelect;
+import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardFilterPattern;
/**
* This service implements the feature handling. It keeps track of all
@@ -57,32 +60,35 @@
@Component(service = {Features.class, Filter.class, Servlet.class},
configurationPolicy = ConfigurationPolicy.IGNORE,
property = {
- HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT + "=(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=org.apache.sling)",
- HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN + "=/",
"felix.webconsole.label=features",
"felix.webconsole.title=Features",
- "felix.webconsole.category=Sling",
- Constants.SERVICE_RANKING + ":Integer=16384",
- Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
+ "felix.webconsole.category=Sling"
})
-public class FeatureManager implements Features, Filter, Servlet {
+@HttpWhiteboardContextSelect("(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=org.apache.sling)")
+@HttpWhiteboardFilterPattern("/")
+@ServiceRanking(16384)
+public class FeatureManager
+ extends GenericServlet
+ implements Features, Filter {
- private final Logger logger = LoggerFactory.getLogger(this.getClass());
+ private final ThreadLocal<ExecutionContextImpl> perThreadClientContext = new ThreadLocal<>();
- private final ThreadLocal<ExecutionContextImpl> perThreadClientContext = new ThreadLocal<ExecutionContextImpl>();
+ private final ConcurrentMap<Long, String> idToNameMap = new ConcurrentHashMap<>();
- private final Map<String, List<FeatureDescription>> allFeatures = new HashMap<String, List<FeatureDescription>>();
+ private final ConcurrentMap<String, List<FeatureDescription>> registeredFeatures = new ConcurrentHashMap<>();
- private Map<String, Feature> activeFeatures = Collections.emptyMap();
-
- private ServletConfig servletConfig;
+ private final ConcurrentMap<String, Feature> activeFeatures = new ConcurrentHashMap<>();
//--- Features
@Override
public Feature[] getFeatures() {
- final Map<String, Feature> activeFeatures = this.activeFeatures;
- return activeFeatures.values().toArray(new Feature[activeFeatures.size()]);
+ return this.activeFeatures.values().toArray(new Feature[activeFeatures.size()]);
+ }
+
+ @Override
+ public Collection<Feature> getAllFeatures() {
+ return Collections.unmodifiableCollection(this.activeFeatures.values());
}
@Override
@@ -122,40 +128,23 @@
@Override
public void destroy() {
// method shared by Servlet and Filter interface
- this.servletConfig = null;
}
//--- Servlet
@Override
- public void init(final ServletConfig config) {
- this.servletConfig = config;
- }
-
- @Override
- public ServletConfig getServletConfig() {
- return this.servletConfig;
- }
-
- @Override
- public String getServletInfo() {
- return "Features";
- }
-
- @Override
public void service(ServletRequest req, ServletResponse res) throws IOException {
if ("GET".equals(((HttpServletRequest) req).getMethod())) {
final PrintWriter pw = res.getWriter();
- final Feature[] features = getFeatures();
- if (features == null || features.length == 0) {
+ if (this.activeFeatures.isEmpty()) {
pw.println("<p class='statline ui-state-highlight'>No Features currently defined</p>");
} else {
pw.printf("<p class='statline ui-state-highlight'>%d Feature(s) currently defined</p>%n",
- features.length);
+ this.activeFeatures.size());
pw.println("<table class='nicetable'>");
pw.println("<tr><th>Name</th><th>Description</th><th>Enabled</th></tr>");
final ExecutionContextImpl ctx = getCurrentExecutionContext();
- for (final Feature feature : features) {
+ for (final Feature feature : this.activeFeatures.values()) {
pw.printf("<tr><td>%s</td><td>%s</td><td>%s</td></tr>%n", ResponseUtil.escapeXml(feature.getName()),
ResponseUtil.escapeXml(feature.getDescription()), ctx.isEnabled(feature));
}
@@ -171,56 +160,47 @@
// bind method for Feature services
@Reference(cardinality = ReferenceCardinality.MULTIPLE,
- policy = ReferencePolicy.DYNAMIC)
- private void bindFeature(final Feature f, final Map<String, Object> props) {
- synchronized (this.allFeatures) {
- final String name = f.getName();
+ policy = ReferencePolicy.DYNAMIC,
+ updated = "updatedFeature")
+ void bindFeature(final Feature f, final Map<String, Object> props) {
+ final String name = f.getName();
+ if ( name != null && !name.isEmpty() ) {
final FeatureDescription info = new FeatureDescription(f, props);
-
- List<FeatureDescription> candidates = this.allFeatures.get(name);
- if (candidates == null) {
- candidates = new ArrayList<FeatureDescription>();
- this.allFeatures.put(name, candidates);
+ this.idToNameMap.put(info.serviceId, name);
+ final List<FeatureDescription> candidates = this.registeredFeatures.computeIfAbsent(name, key -> new ArrayList<>());
+ synchronized (candidates) {
+ candidates.add(info);
+ Collections.sort(candidates);
+ this.activeFeatures.put(name, candidates.get(0).feature);
}
- candidates.add(info);
- Collections.sort(candidates);
-
- this.calculateActiveProviders();
}
}
+ // updated method for Feature services
+ void updatedFeature(final Feature f, final Map<String, Object> props) {
+ this.unbindFeature(f, props);
+ this.bindFeature(f, props);
+ }
+
// unbind method for Feature services
- @SuppressWarnings("unused")
- private void unbindFeature(final Feature f, final Map<String, Object> props) {
- synchronized (this.allFeatures) {
- final String name = f.getName();
- final FeatureDescription info = new FeatureDescription(f, props);
-
- final List<FeatureDescription> candidates = this.allFeatures.get(name);
+ void unbindFeature(final Feature f, final Map<String, Object> props) {
+ final FeatureDescription info = new FeatureDescription(f, props);
+ final String name = this.idToNameMap.remove(info.serviceId);
+ if ( name != null ) {
+ final List<FeatureDescription> candidates = this.registeredFeatures.get(name);
if (candidates != null) { // sanity check
- candidates.remove(info);
- if (candidates.size() == 0) {
- this.allFeatures.remove(name);
+ synchronized ( candidates ) {
+ candidates.remove(info);
+ if ( candidates.isEmpty() ) {
+ this.activeFeatures.remove(name);
+ } else {
+ this.activeFeatures.put(name, candidates.get(0).feature);
+ }
}
}
- this.calculateActiveProviders();
}
}
- // calculates map of active features (eliminating Feature name
- // collisions). Must be called while synchronized on this.allFeatures
- private void calculateActiveProviders() {
- final Map<String, Feature> activeMap = new HashMap<String, Feature>();
- for (final Map.Entry<String, List<FeatureDescription>> entry : this.allFeatures.entrySet()) {
- final FeatureDescription desc = entry.getValue().get(0);
- activeMap.put(entry.getKey(), desc.feature);
- if (entry.getValue().size() > 1) {
- logger.warn("More than one feature service for feature {}", entry.getKey());
- }
- }
- this.activeFeatures = activeMap;
- }
-
//--- Client Context management and access
void pushContext(final HttpServletRequest request) {
@@ -242,7 +222,7 @@
*/
private final static class FeatureDescription implements Comparable<FeatureDescription> {
- public final int ranking;
+ private final int ranking;
public final long serviceId;
diff --git a/src/main/java/org/apache/sling/featureflags/package-info.java b/src/main/java/org/apache/sling/featureflags/package-info.java
index 4df4dc6..10dfe08 100644
--- a/src/main/java/org/apache/sling/featureflags/package-info.java
+++ b/src/main/java/org/apache/sling/featureflags/package-info.java
@@ -63,5 +63,5 @@
* @version 1.1
* @see <a href="http://sling.apache.org/documentation/the-sling-engine/featureflags.html">Feature Flags</a>
*/
-@org.osgi.annotation.versioning.Version("1.1.0")
+@org.osgi.annotation.versioning.Version("1.2.0")
package org.apache.sling.featureflags;
diff --git a/src/test/java/org/apache/sling/featureflags/impl/FeatureManagerTest.java b/src/test/java/org/apache/sling/featureflags/impl/FeatureManagerTest.java
new file mode 100644
index 0000000..823cab8
--- /dev/null
+++ b/src/test/java/org/apache/sling/featureflags/impl/FeatureManagerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.featureflags.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+import java.util.Collections;
+
+import org.apache.sling.featureflags.Feature;
+import org.apache.sling.featureflags.Features;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.Constants;
+
+public class FeatureManagerTest {
+
+ private Feature createFeature(final String name) {
+ final Feature f = Mockito.mock(Feature.class);
+ Mockito.when(f.getName()).thenReturn(name);
+ return f;
+ }
+
+ @Test
+ public void testFeatures() {
+ final Features features = new FeatureManager();
+ assertEquals(0, features.getAllFeatures().size());
+ }
+
+ @Test
+ public void testDuplicateFeatureName() {
+ final FeatureManager features = new FeatureManager();
+ final Feature f1 = createFeature("a");
+ final Feature f2 = createFeature("b");
+ final Feature f3 = createFeature("b");
+ features.bindFeature(f1, Collections.singletonMap(Constants.SERVICE_ID, 1L));
+ features.bindFeature(f2, Collections.singletonMap(Constants.SERVICE_ID, 2L));
+ features.bindFeature(f3, Collections.singletonMap(Constants.SERVICE_ID, 3L));
+
+ // a and b
+ assertEquals(2, features.getAllFeatures().size());
+ assertNotNull(features.getFeature("a"));
+ assertSame(f1, features.getFeature("a"));
+ assertNotNull(features.getFeature("b"));
+ assertSame(f2, features.getFeature("b"));
+
+ // remove a - b remains
+ features.unbindFeature(f1, Collections.singletonMap(Constants.SERVICE_ID, 1L));
+ assertEquals(1, features.getAllFeatures().size());
+ assertNotNull(features.getFeature("b"));
+ assertSame(f2, features.getFeature("b"));
+
+ // remove higher b - lower b remains
+ features.unbindFeature(f2, Collections.singletonMap(Constants.SERVICE_ID, 2L));
+ assertEquals(1, features.getAllFeatures().size());
+ assertNotNull(features.getFeature("b"));
+ assertSame(f3, features.getFeature("b"));
+
+ // remove lower b - no features
+ features.unbindFeature(f3, Collections.singletonMap(Constants.SERVICE_ID, 3L));
+ assertEquals(0, features.getAllFeatures().size());
+ }
+}