This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.featureflags-1.0.0 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-featureflags.git
commit 5e66975857d4ad36946668ae7da2cb9e125acdbb Author: Felix Meschberger <[email protected]> AuthorDate: Tue Jan 28 08:32:39 2014 +0000 SLING-3148 Merge back from whiteboard/fmeschbe/featureflags/feature-flags - Removed ResourceHiding and ResourceTypeHiding - Added ResourceDecorator support - Added WebConsole plugin - Added Feature factory configuration support - Added JavaDoc - Support non-Sling ExecutionContext - Porperly register ClientContext filter as Sling filter git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/feature-flags@1561994 13f79535-47bb-0310-9956-ffa450edef68 --- pom.xml | 10 +- .../apache/sling/featureflags/ClientContext.java | 22 +- .../sling/featureflags/ExecutionContext.java | 35 ++- .../org/apache/sling/featureflags/Feature.java | 38 ++-- .../org/apache/sling/featureflags/Features.java | 81 +++++-- .../apache/sling/featureflags/ResourceHiding.java | 41 ---- .../sling/featureflags/ResourceTypeMapping.java | 41 ---- .../sling/featureflags/impl/ClientContextImpl.java | 49 ++--- .../sling/featureflags/impl/ConfiguredFeature.java | 105 +++++++++ .../impl/CurrentClientContextFilter.java | 43 ++-- .../featureflags/impl/ExecutionContextImpl.java | 12 +- .../sling/featureflags/impl/FeatureManager.java | 242 ++++++++++++--------- ...atorImpl.java => FeatureResourceDecorator.java} | 40 ++-- .../featureflags/impl/FeatureWebConsolePlugin.java | 74 +++++++ .../sling/featureflags/impl/FeaturesImpl.java | 17 +- .../featureflags/impl/ResourceAccessImpl.java | 58 ----- .../apache/sling/featureflags/package-info.java | 42 +++- 17 files changed, 563 insertions(+), 387 deletions(-) diff --git a/pom.xml b/pom.xml index 7f62fee..75a28cd 100644 --- a/pom.xml +++ b/pom.xml @@ -43,19 +43,13 @@ <dependency> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.api</artifactId> - <version>2.5.0</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>org.apache.sling</groupId> - <artifactId>org.apache.sling.resourceaccesssecurity</artifactId> - <version>0.0.1-SNAPSHOT</version> + <version>2.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.commons.osgi</artifactId> - <version>2.2.0</version> + <version>2.1.0</version> <scope>provided</scope> </dependency> <dependency> diff --git a/src/main/java/org/apache/sling/featureflags/ClientContext.java b/src/main/java/org/apache/sling/featureflags/ClientContext.java index 61d5814..137e6ff 100644 --- a/src/main/java/org/apache/sling/featureflags/ClientContext.java +++ b/src/main/java/org/apache/sling/featureflags/ClientContext.java @@ -23,21 +23,31 @@ import java.util.Collection; import aQute.bnd.annotation.ProviderType; /** - * The client context can be used by client code to check whether - * a specific feature is enable. - * A client context can be created through the {@link Features} service. + * The client context can be used by client code to check whether a specific + * feature is enable. + * <p> + * Prepared {@code ClientContext} instances are available through the + * {@link Features} service. Consumers of this interface are not expected to + * implent it. */ @ProviderType public interface ClientContext { /** - * Returns <code>true</code> if the feature is enabled. + * Returns {@code true} if the named feature is enabled. + * + * @param featureName The name of the feature. + * @return {@code true} if the named feature is enabled. {@code false} is + * returned if the named feature is not enabled, is not known or the + * {@code featureName} parameter is {@code null} or an empty String. */ boolean isEnabled(String featureName); /** - * Returns a list of all enabled features - * @return The list of features, the list might be empty. + * Returns a possibly empty collection of enabled {@link Feature} instances. + * + * @return The collection of enabled {@link Feature} instances. This + * collection may be empty and is not modifiable. */ Collection<Feature> getEnabledFeatures(); } diff --git a/src/main/java/org/apache/sling/featureflags/ExecutionContext.java b/src/main/java/org/apache/sling/featureflags/ExecutionContext.java index d01bd67..14d684d 100644 --- a/src/main/java/org/apache/sling/featureflags/ExecutionContext.java +++ b/src/main/java/org/apache/sling/featureflags/ExecutionContext.java @@ -18,26 +18,49 @@ */ package org.apache.sling.featureflags; -import org.apache.sling.api.SlingHttpServletRequest; +import javax.servlet.http.HttpServletRequest; + import org.apache.sling.api.resource.ResourceResolver; import aQute.bnd.annotation.ProviderType; /** - * The provider context contains all information that is passed to a - * {@link Feature} in order to check whether a feature is enabled. + * The {@code ExecutionContext} interface provides access to the context for + * evaluating whether a feature is enabled or not. Instances of this object are + * provided to the {@link Feature#isEnabled(ExecutionContext)} to help + * evaluating whether the feature is enabled or not. + * <p> + * The {@link Features} service {@link ClientContext} generating methods create + * an instance of this to collect the enabled {@link Feature} services for the + * creation of the {@link ClientContext} instance. + * <p> + * This object provides access to live data and must only be used to read + * information. Modifying content through a {@code ResourceResolver} directly or + * indirectly provided by this object is considered inappropriate and faulty + * behaviour. */ @ProviderType public interface ExecutionContext { /** - * Return the associated request if available + * Returns a {@code HttpServletRequest} object to retrieve information which + * may influence the decision whether a {@link Feature} is enabled or not. + * If a {@code HttpServletRequest} object is not available in the context, + * this method may return {@code null}. + * <p> + * In a Sling request processing context, the {@code HttpServletRequest} + * object returned may actually be a {@code SlingHttpServletRequest}. + * * @return the request or <code>null</code> */ - SlingHttpServletRequest getRequest(); + HttpServletRequest getRequest(); /** - * Return the associated resource resolver. + * Returns a {@code ResourceResolver} object to retrieve information which + * may influence the decision whether a {@link Feature} is enabled or not. + * If a {@code ResourceResolver} object is not available in the context, + * this method may return {@code null}. + * * @return the resource resolver */ ResourceResolver getResourceResolver(); diff --git a/src/main/java/org/apache/sling/featureflags/Feature.java b/src/main/java/org/apache/sling/featureflags/Feature.java index 5bec8fd..3984df5 100644 --- a/src/main/java/org/apache/sling/featureflags/Feature.java +++ b/src/main/java/org/apache/sling/featureflags/Feature.java @@ -18,37 +18,47 @@ */ package org.apache.sling.featureflags; -import org.apache.sling.api.adapter.Adaptable; - import aQute.bnd.annotation.ConsumerType; /** - * A feature is defined by its name. - * Depending on the functionality the feature implements it can - * be adapted to different services, like - * <ul> - * <li>{@link ResourceHiding}</li> - * <li>{@link ResourceTypeMapping}</li> - * </ul> - * - * Features are registered as OSGi services. + * A feature is defined by its name. Features are registered as OSGi services. + * This interface is expected to be implemented by feature providers. + * <p> + * Feature {@link #getName() names} should be globally unique. If multiple + * features have the same name, the feature with the highest service ranking is + * accessible through the {@link Features} service and the {@link ClientContext}. */ @ConsumerType -public interface Feature extends Adaptable { +public interface Feature { /** - * Checks whether the feature is enabled for the current execution - * context. + * Checks whether the feature is enabled for the given execution context. + * <p> + * Multiple calls to this method may but are not required to return the same + * value. For example the return value may depend on the time of day, some + * random number or some information provided by the given + * {@link ExecutionContext}. + * + * @param context The {@link ExecutionContext} providing a context to + * evaluate whether the feature is enabled or not. + * @return {@code true} if this {@code Feature} is enabled in the given + * {@link ExecutionContext}. */ boolean isEnabled(ExecutionContext context); /** * The name of the feature. + * + * @return The name of this feature which must not be {@code null} or an + * empty string. */ String getName(); /** * The description of the feature. + * + * @return The optional description of this feature, which may be + * {@code null} or an empty string. */ String getDescription(); } diff --git a/src/main/java/org/apache/sling/featureflags/Features.java b/src/main/java/org/apache/sling/featureflags/Features.java index 8b6a810..eb5faac 100644 --- a/src/main/java/org/apache/sling/featureflags/Features.java +++ b/src/main/java/org/apache/sling/featureflags/Features.java @@ -18,59 +18,108 @@ */ package org.apache.sling.featureflags; -import org.apache.sling.api.SlingHttpServletRequest; +import javax.servlet.http.HttpServletRequest; + import org.apache.sling.api.resource.ResourceResolver; import aQute.bnd.annotation.ProviderType; /** - * The features service is the central gateway for feature handling. - * It can be used to query the available features and to create - * client contexts to be used for enabled feature checking. + * The features service is the central gateway for feature handling. It can be + * used to query the available features and to create client contexts to be used + * for enabled feature checking. */ @ProviderType public interface Features { /** - * Get the list of all available feature names. A feature is available - * if there is a registered {@link Feature} service. + * Get the list of all available (known) feature names. + * <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 names of the known features */ String[] getAvailableFeatureNames(); /** - * Get the list of all available features. A feature is available - * if there is a registered {@link Feature} service. + * Get the list of all available (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 */ Feature[] getAvailableFeatures(); /** * Returns the feature with the given name. - * @return The feature or <code>null</code> + * + * @param name The name of the feature. + * @return The feature or <code>null</code> if not known or the name is an + * empty string or {@code null}. */ Feature getFeature(String name); /** - * Checks whether a feature with the given name is available. - * A feature is available if there is a registered {@link Feature} service. + * Checks whether a feature with the given name is available (known). + * <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}. + * + * @param featureName The name of the feature to check for availability. + * @return {@code true} if the named feature is available. */ boolean isAvailable(String featureName); /** - * Returns the current client context. - * This method always returns a client context object + * Returns the current client context. This method always returns a client + * context object. + * * @return A client context. */ ClientContext getCurrentClientContext(); /** * Create a client context for the resource resolver. - * @throws IllegalArgumentException If resolver is null + * <p> + * The {@link ClientContext} is a snapshot of the enablement state of the + * features at the time of creation. A change in the feature enablement + * state is not reflected in {@link ClientContext} objects created prior to + * changing the state. + * <p> + * The {@link ClientContext} returned is not available through the + * {@link #getCurrentClientContext()} method. + * + * @param resolver The {@code ResourceResolver} to base the + * {@link ClientContext} on. + * @return A newly created client context based on the given + * {@code ResourceResolver}. + * @throws IllegalArgumentException If {@code resolver} is {@code null} */ ClientContext createClientContext(ResourceResolver resolver); /** * Create a client context for the request. - * @throws IllegalArgumentException If request is null + * <p> + * The {@link ClientContext} is a snapshot of the enablement state of the + * features at the time of creation. A change in the feature enablement + * state is not reflected in {@link ClientContext} objects created prior to + * changing the state. + * <p> + * The {@link ClientContext} returned is not available through the + * {@link #getCurrentClientContext()} method. + * + * @param request The {@code HttpServletRequest} to base the + * {@link ClientContext} on. If this is a + * {@code SlingHttpServletContext} the {@link ClientContext}'s + * resource resolver is set to the request's resource resolver. + * @return A newly created client context based on the given + * {@code HttpServletRequest}. + * @throws IllegalArgumentException If {@code request} is {@code null} */ - ClientContext createClientContext(SlingHttpServletRequest request); + ClientContext createClientContext(HttpServletRequest request); } diff --git a/src/main/java/org/apache/sling/featureflags/ResourceHiding.java b/src/main/java/org/apache/sling/featureflags/ResourceHiding.java deleted file mode 100644 index d19fab6..0000000 --- a/src/main/java/org/apache/sling/featureflags/ResourceHiding.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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; - -import org.apache.sling.api.resource.Resource; - -import aQute.bnd.annotation.ConsumerType; - -/** - * A {@link Feature} which is hiding resources can be adapted to - * this service interface. - */ -@ConsumerType -public interface ResourceHiding { - - /** - * Checks whether a resource should be hidden for a feature. - * This check is only executed if {@link Feature#isEnabled(ExecutionContext)} - * return true for the given feature/context. The caller of this - * method must ensure to call {@link Feature#isEnabled(ExecutionContext)} - * before calling this method and only call this method if - * {@link Feature#isEnabled(ExecutionContext)} returned <code>true</code> - */ - boolean hideResource(Resource resource); -} diff --git a/src/main/java/org/apache/sling/featureflags/ResourceTypeMapping.java b/src/main/java/org/apache/sling/featureflags/ResourceTypeMapping.java deleted file mode 100644 index a9ca5f4..0000000 --- a/src/main/java/org/apache/sling/featureflags/ResourceTypeMapping.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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; - -import java.util.Map; - -import aQute.bnd.annotation.ConsumerType; - -/** - * A {@link Feature} which is mapping resource types can be adapted to - * this service interface. - */ -@ConsumerType -public interface ResourceTypeMapping { - - /** - * Returns the resource type mapping for a feature. - * This mapping is only executed if {@link Feature#isEnabled(ExecutionContext)} - * return true for the given feature/context. The caller of this - * method must ensure to call {@link Feature#isEnabled(ExecutionContext)} - * before calling this method and only call this method if - * {@link Feature#isEnabled(ExecutionContext)} returned <code>true</code> - */ - Map<String, String> getResourceTypeMapping(); -} diff --git a/src/main/java/org/apache/sling/featureflags/impl/ClientContextImpl.java b/src/main/java/org/apache/sling/featureflags/impl/ClientContextImpl.java index b71ebe4..dc02b25 100644 --- a/src/main/java/org/apache/sling/featureflags/impl/ClientContextImpl.java +++ b/src/main/java/org/apache/sling/featureflags/impl/ClientContextImpl.java @@ -21,15 +21,13 @@ package org.apache.sling.featureflags.impl; 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 org.apache.sling.api.resource.ResourceDecorator; import org.apache.sling.featureflags.ClientContext; import org.apache.sling.featureflags.Feature; import org.apache.sling.featureflags.ExecutionContext; -import org.apache.sling.featureflags.ResourceHiding; -import org.apache.sling.featureflags.ResourceTypeMapping; /** * Implementation of the client context @@ -38,28 +36,22 @@ public class ClientContextImpl implements ClientContext { private final ExecutionContext featureContext; - private final List<Feature> enabledFeatures; + private final Map<String, Feature> enabledFeatures; - private final List<ResourceHiding> hidingFeatures; + private final List<ResourceDecorator> resourceDecorators; - private final Map<String, String> mapperFeatures = new HashMap<String, String>(); - - public ClientContextImpl(final ExecutionContext featureContext, final List<Feature> features) { - this.enabledFeatures = Collections.unmodifiableList(features); - final List<ResourceHiding> hiding = new ArrayList<ResourceHiding>(); - for(final Feature f : this.enabledFeatures) { - final ResourceHiding rh = f.adaptTo(ResourceHiding.class); - if ( rh != null ) { - hiding.add(rh); - } - final ResourceTypeMapping rm = f.adaptTo(ResourceTypeMapping.class); - if ( rm != null ) { - final Map<String, String> mapping = rm.getResourceTypeMapping(); - mapperFeatures.putAll(mapping); + public ClientContextImpl(final ExecutionContext featureContext, final Map<String, Feature> features) { + ArrayList<ResourceDecorator> resourceDecorators = new ArrayList<ResourceDecorator>(features.size()); + for (final Feature f : features.values()) { + if (f instanceof ResourceDecorator) { + resourceDecorators.add((ResourceDecorator) f); } } - this.hidingFeatures = hiding; + resourceDecorators.trimToSize(); + this.featureContext = featureContext; + this.enabledFeatures = Collections.unmodifiableMap(features); + this.resourceDecorators = Collections.unmodifiableList(resourceDecorators); } public ExecutionContext getFeatureContext() { @@ -68,24 +60,15 @@ public class ClientContextImpl implements ClientContext { @Override public boolean isEnabled(final String featureName) { - for(final Feature f : this.enabledFeatures) { - if ( featureName.equals(f.getName()) ) { - return true; - } - } - return false; + return this.enabledFeatures.get(featureName) != null; } @Override public Collection<Feature> getEnabledFeatures() { - return this.enabledFeatures; - } - - public Collection<ResourceHiding> getHidingFeatures() { - return this.hidingFeatures; + return this.enabledFeatures.values(); } - public Map<String, String> getResourceTypeMapping() { - return this.mapperFeatures; + public List<ResourceDecorator> getResourceDecorators() { + return this.resourceDecorators; } } diff --git a/src/main/java/org/apache/sling/featureflags/impl/ConfiguredFeature.java b/src/main/java/org/apache/sling/featureflags/impl/ConfiguredFeature.java new file mode 100644 index 0000000..1f4db86 --- /dev/null +++ b/src/main/java/org/apache/sling/featureflags/impl/ConfiguredFeature.java @@ -0,0 +1,105 @@ +/* + * 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 java.util.Map; + +import javax.servlet.ServletRequest; + +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.ConfigurationPolicy; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.Service; +import org.apache.sling.commons.osgi.PropertiesUtil; +import org.apache.sling.featureflags.ExecutionContext; +import org.apache.sling.featureflags.Feature; +import org.osgi.framework.Constants; + +@Component( + name = "org.apache.sling.featureflags.Feature", + metatype = true, + configurationFactory = true, + policy = ConfigurationPolicy.REQUIRE, + label = "Statically Configured Feature", + description = "Allows for the definition of statically configured features which are defined and enabled through OSGi configuration") +@Service +public class ConfiguredFeature implements Feature { + + private static final String PROP_FEATURE = "feature"; + + @Property(label = "Name", description = "Short name of this feature. This name is used " + + "to refer to this feature when checking for it to be enabled or not. This " + + "property is required and defaults to a name derived from the feature's class " + + "name and object identity. It is strongly recommended to define a useful and unique for the feature") + private static final String NAME = "name"; + + @Property(label = "Description", description = "Description for the feature. The " + + "intent is to descibe the behaviour of the application if this feature would be " + + "enabled. It is recommended to define this property. The default value is the value of the name property.") + private static final String DESCRIPTION = "description"; + + @Property(boolValue = false, label = "Enabled", description = "Boolean flag indicating whether the feature is " + + "enabled or not by this configuration") + private static final String ENABLED = "enabled"; + + private String name; + + private String description; + + private boolean enabled; + + @Activate + private void activate(final Map<String, Object> configuration) { + final String pid = PropertiesUtil.toString(configuration.get(Constants.SERVICE_PID), getClass().getName() + "$" + + System.identityHashCode(this)); + this.name = PropertiesUtil.toString(configuration.get(NAME), pid); + this.description = PropertiesUtil.toString(configuration.get(DESCRIPTION), this.name); + this.enabled = PropertiesUtil.toBoolean(configuration.get(ENABLED), false); + } + + @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; + } + } + } + } + + return this.enabled; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String getDescription() { + return this.description; + } +} diff --git a/src/main/java/org/apache/sling/featureflags/impl/CurrentClientContextFilter.java b/src/main/java/org/apache/sling/featureflags/impl/CurrentClientContextFilter.java index 9399666..da6287f 100644 --- a/src/main/java/org/apache/sling/featureflags/impl/CurrentClientContextFilter.java +++ b/src/main/java/org/apache/sling/featureflags/impl/CurrentClientContextFilter.java @@ -26,45 +26,38 @@ import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; - -import org.apache.felix.scr.annotations.Component; -import org.apache.felix.scr.annotations.Property; -import org.apache.felix.scr.annotations.Reference; -import org.apache.felix.scr.annotations.Service; -import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.featureflags.ClientContext; /** - * This general servlet filter sets the current client context to the - * current request. + * This general servlet filter sets the current client context to the current + * request. */ -@Component -@Service(value=Filter.class) -@Property(name="pattern", value="/.*") public class CurrentClientContextFilter implements Filter { - @Reference - private FeatureManager manager; + private final FeatureManager featureManager; + + public CurrentClientContextFilter(final FeatureManager featureManager) { + this.featureManager = featureManager; + } @Override - public void doFilter(final ServletRequest req, final ServletResponse res, - final FilterChain chain) - throws IOException, ServletException { - if ( req instanceof SlingHttpServletRequest ) { - manager.setCurrentClientContext((SlingHttpServletRequest)req); - } + public void init(final FilterConfig config) { + // nothing to do + } + + @Override + public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain) + throws IOException, ServletException { + + ClientContext current = this.featureManager.setCurrentClientContext(req); try { chain.doFilter(req, res); } finally { - manager.unsetCurrentClientContext(); + this.featureManager.unsetCurrentClientContext(current); } } @Override - public void init(final FilterConfig config) throws ServletException { - // nothing to do - } - - @Override public void destroy() { // nothing to do } 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 555575f..cfd673a 100644 --- a/src/main/java/org/apache/sling/featureflags/impl/ExecutionContextImpl.java +++ b/src/main/java/org/apache/sling/featureflags/impl/ExecutionContextImpl.java @@ -18,6 +18,8 @@ */ package org.apache.sling.featureflags.impl; +import javax.servlet.http.HttpServletRequest; + import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.featureflags.ExecutionContext; @@ -29,20 +31,22 @@ public class ExecutionContextImpl implements ExecutionContext { private final ResourceResolver resourceResolver; - private final SlingHttpServletRequest request; + private final HttpServletRequest request; public ExecutionContextImpl(final ResourceResolver resourceResolver) { this.request = null; this.resourceResolver = resourceResolver; } - public ExecutionContextImpl(final SlingHttpServletRequest request) { + public ExecutionContextImpl(final HttpServletRequest request) { this.request = request; - this.resourceResolver = request.getResourceResolver(); + this.resourceResolver = (request instanceof SlingHttpServletRequest) + ? ((SlingHttpServletRequest) request).getResourceResolver() + : null; } @Override - public SlingHttpServletRequest getRequest() { + public HttpServletRequest getRequest() { return this.request; } 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 fd601f6..61b5641 100644 --- a/src/main/java/org/apache/sling/featureflags/impl/FeatureManager.java +++ b/src/main/java/org/apache/sling/featureflags/impl/FeatureManager.java @@ -22,51 +22,116 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Hashtable; import java.util.List; import java.util.Map; -import java.util.TreeMap; +import javax.servlet.Filter; +import javax.servlet.Servlet; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferenceCardinality; import org.apache.felix.scr.annotations.ReferencePolicy; -import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.ResourceDecorator; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.featureflags.ClientContext; +import org.apache.sling.featureflags.ExecutionContext; import org.apache.sling.featureflags.Feature; import org.apache.sling.featureflags.Features; -import org.apache.sling.featureflags.ExecutionContext; +import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * This service implements the feature handling. - * It keeps track of all {@link Feature} services. + * This service implements the feature handling. It keeps track of all + * {@link Feature} services. */ @Component -@Reference(name="feature", - cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE, - policy=ReferencePolicy.DYNAMIC, - referenceInterface=Feature.class) -public class FeatureManager implements Features { +@Reference( + name = "feature", + cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, + policy = ReferencePolicy.DYNAMIC, + referenceInterface = Feature.class) +public class FeatureManager { private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private final ThreadLocal<ClientContext> perThreadClientContext = new ThreadLocal<ClientContext>(); + + private final ClientContext defaultClientContext = new ClientContext() { + @Override + public boolean isEnabled(final String featureName) { + return false; + } + + @Override + public Collection<Feature> getEnabledFeatures() { + return Collections.emptyList(); + } + }; + private final Map<String, List<FeatureDescription>> allFeatures = new HashMap<String, List<FeatureDescription>>(); - private Map<String, FeatureDescription> activeFeatures = new TreeMap<String, FeatureDescription>(); + private Map<String, Feature> activeFeatures = Collections.emptyMap(); + + private List<ServiceRegistration> services; + + @SuppressWarnings("serial") + @Activate + private void activate(BundleContext bundleContext) { + ArrayList<ServiceRegistration> services = new ArrayList<ServiceRegistration>(); + services.add(bundleContext.registerService(Features.class.getName(), new FeaturesImpl(this), null)); + services.add(bundleContext.registerService(ResourceDecorator.class.getName(), + new FeatureResourceDecorator(this), null)); + services.add(bundleContext.registerService(Servlet.class.getName(), new FeatureWebConsolePlugin(this), + new Hashtable<String, Object>() { + { + put("felix.webconsole.label", "features"); + put("felix.webconsole.title", "Features"); + put("felix.webconsole.category", "Sling"); + } + })); + services.add(bundleContext.registerService(Filter.class.getName(), new CurrentClientContextFilter(this), + new Hashtable<String, Object>() { + { + put("sling.filter.scope", "REQUEST"); + put("service.ranking", Integer.MIN_VALUE); + } + })); + this.services = services; + } - /** - * Bind a new feature - */ - protected void bindFeature(final Feature f, final Map<String, Object> props) { - synchronized ( this.allFeatures ) { + @Deactivate + private void deactivate() { + if (this.services != null) { + for (ServiceRegistration service : this.services) { + if (service != null) { + service.unregister(); + } + } + this.services.clear(); + this.services = null; + } + } + + //--- Feature binding + + // bind method for Feature services + @SuppressWarnings("unused") + private void bindFeature(final Feature f, final Map<String, Object> props) { + synchronized (this.allFeatures) { final String name = f.getName(); final FeatureDescription info = new FeatureDescription(f, props); List<FeatureDescription> candidates = this.allFeatures.get(name); - if ( candidates == null ) { + if (candidates == null) { candidates = new ArrayList<FeatureDescription>(); this.allFeatures.put(name, candidates); } @@ -77,18 +142,17 @@ public class FeatureManager implements Features { } } - /** - * Unbind a feature - */ - protected void unbindFeature(final Feature f, final Map<String, Object> props) { - synchronized ( this.allFeatures ) { + // 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); - if ( candidates != null ) { // sanity check + if (candidates != null) { // sanity check candidates.remove(info); - if ( candidates.size() == 0 ) { + if (candidates.size() == 0) { this.allFeatures.remove(name); } } @@ -96,56 +160,50 @@ public class FeatureManager implements Features { } } + // calculates map of active features (eliminating Feature name + // collisions). Must be called while synchronized on this.allFeatures private void calculateActiveProviders() { - final Map<String, FeatureDescription> activeMap = new TreeMap<String, FeatureDescription>(); - for(final Map.Entry<String, List<FeatureDescription>> entry : this.allFeatures.entrySet()) { + 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); - if ( entry.getValue().size() > 1 ) { + 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; } - private final ThreadLocal<ClientContextImpl> perThreadClientContext = new ThreadLocal<ClientContextImpl>(); - - private final ClientContext defaultClientContext = new ClientContext() { - - @Override - public boolean isEnabled(final String featureName) { - return false; - } - - @Override - public Collection<Feature> getEnabledFeatures() { - return Collections.emptyList(); - } - }; + //--- Client Context management and access - @Override - public ClientContext getCurrentClientContext() { + ClientContext getCurrentClientContext() { ClientContext result = perThreadClientContext.get(); - if ( result == null ) { + if (result == null) { result = defaultClientContext; } return result; } - public void setCurrentClientContext(final SlingHttpServletRequest request) { - final ExecutionContext providerContext = new ExecutionContextImpl(request); - final ClientContextImpl ctx = this.createClientContext(providerContext); - perThreadClientContext.set(ctx); + ClientContext setCurrentClientContext(final ServletRequest request) { + final ClientContext current = perThreadClientContext.get(); + if (request instanceof HttpServletRequest) { + final ExecutionContext providerContext = new ExecutionContextImpl((HttpServletRequest) request); + final ClientContextImpl ctx = this.createClientContext(providerContext); + perThreadClientContext.set(ctx); + } + return current; } - public void unsetCurrentClientContext() { - perThreadClientContext.remove(); + void unsetCurrentClientContext(final ClientContext previous) { + if (previous != null) { + perThreadClientContext.set(previous); + } else { + perThreadClientContext.remove(); + } } - @Override - public ClientContext createClientContext(final ResourceResolver resolver) { - if ( resolver == null ) { + ClientContext createClientContext(final ResourceResolver resolver) { + if (resolver == null) { throw new IllegalArgumentException("Resolver must not be null."); } final ExecutionContext providerContext = new ExecutionContextImpl(resolver); @@ -153,9 +211,8 @@ public class FeatureManager implements Features { return ctx; } - @Override - public ClientContext createClientContext(final SlingHttpServletRequest request) { - if ( request == null ) { + ClientContext createClientContext(final HttpServletRequest request) { + if (request == null) { throw new IllegalArgumentException("Request must not be null."); } final ExecutionContext providerContext = new ExecutionContextImpl(request); @@ -164,75 +221,64 @@ public class FeatureManager implements Features { } private ClientContextImpl createClientContext(final ExecutionContext providerContext) { - final List<Feature> enabledFeatures = new ArrayList<Feature>(); - - for(final Map.Entry<String, FeatureDescription> entry : this.activeFeatures.entrySet()) { - final Feature f = entry.getValue().feature; - - if ( entry.getValue().feature.isEnabled(providerContext) ) { - enabledFeatures.add(f); + final Map<String, Feature> enabledFeatures = new HashMap<String, Feature>(); + for (final Map.Entry<String, Feature> entry : this.activeFeatures.entrySet()) { + if (entry.getValue().isEnabled(providerContext)) { + enabledFeatures.put(entry.getKey(), entry.getValue()); } } - final ClientContextImpl ctx = new ClientContextImpl(providerContext, enabledFeatures); - return ctx; + return new ClientContextImpl(providerContext, enabledFeatures); } - @Override - public Feature[] getAvailableFeatures() { - final List<Feature> result = new ArrayList<Feature>(); - for(final Map.Entry<String, FeatureDescription> entry : this.activeFeatures.entrySet()) { - final Feature f = entry.getValue().feature; - result.add(f); - } - return result.toArray(new Feature[result.size()]); + //--- Feature access + + Feature[] getAvailableFeatures() { + final Map<String, Feature> activeFeatures = this.activeFeatures; + return activeFeatures.values().toArray(new Feature[activeFeatures.size()]); } - @Override - public Feature getFeature(final String name) { - final FeatureDescription desc = this.activeFeatures.get(name); - if ( desc != null ) { - return desc.feature; - } - return null; + Feature getFeature(final String name) { + return this.activeFeatures.get(name); } - @Override - public String[] getAvailableFeatureNames() { - return this.activeFeatures.keySet().toArray(new String[this.activeFeatures.size()]); + String[] getAvailableFeatureNames() { + final Map<String, Feature> activeFeatures = this.activeFeatures; + return activeFeatures.keySet().toArray(new String[activeFeatures.size()]); } - @Override - public boolean isAvailable(final String featureName) { + boolean isAvailable(final String featureName) { return this.activeFeatures.containsKey(featureName); } /** - * Internal class caching some feature meta data like service id and ranking. + * Internal class caching some feature meta data like service id and + * ranking. */ private final static class FeatureDescription implements Comparable<FeatureDescription> { public final int ranking; + public final long serviceId; + public final Feature feature; - public FeatureDescription(final Feature feature, - final Map<String, Object> props) { + public FeatureDescription(final Feature feature, final Map<String, Object> props) { this.feature = feature; final Object sr = props.get(Constants.SERVICE_RANKING); - if ( sr == null || !(sr instanceof Integer)) { - this.ranking = 0; + if (sr instanceof Integer) { + this.ranking = (Integer) sr; } else { - this.ranking = (Integer)sr; + this.ranking = 0; } - this.serviceId = (Long)props.get(Constants.SERVICE_ID); + this.serviceId = (Long) props.get(Constants.SERVICE_ID); } @Override public int compareTo(final FeatureDescription o) { - if ( this.ranking < o.ranking ) { + if (this.ranking < o.ranking) { return 1; - } else if (this.ranking > o.ranking ) { + } else if (this.ranking > o.ranking) { return -1; } // If ranks are equal, then sort by service id in descending order. @@ -241,8 +287,8 @@ public class FeatureManager implements Features { @Override public boolean equals(final Object obj) { - if ( obj instanceof FeatureDescription ) { - return ((FeatureDescription)obj).serviceId == this.serviceId; + if (obj instanceof FeatureDescription) { + return ((FeatureDescription) obj).serviceId == this.serviceId; } return false; } diff --git a/src/main/java/org/apache/sling/featureflags/impl/ResourceDecoratorImpl.java b/src/main/java/org/apache/sling/featureflags/impl/FeatureResourceDecorator.java similarity index 58% rename from src/main/java/org/apache/sling/featureflags/impl/ResourceDecoratorImpl.java rename to src/main/java/org/apache/sling/featureflags/impl/FeatureResourceDecorator.java index e2f818d..4985ab0 100644 --- a/src/main/java/org/apache/sling/featureflags/impl/ResourceDecoratorImpl.java +++ b/src/main/java/org/apache/sling/featureflags/impl/FeatureResourceDecorator.java @@ -20,46 +20,34 @@ package org.apache.sling.featureflags.impl; import javax.servlet.http.HttpServletRequest; -import org.apache.felix.scr.annotations.Component; -import org.apache.felix.scr.annotations.Reference; -import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceDecorator; -import org.apache.sling.api.resource.ResourceWrapper; import org.apache.sling.featureflags.ClientContext; /** * Resource decorator implementing the resource type mapping */ -@Component -@Service(value=ResourceDecorator.class) -public class ResourceDecoratorImpl implements ResourceDecorator { +public class FeatureResourceDecorator implements ResourceDecorator { - @Reference - private FeatureManager manager; + private final FeatureManager manager; + + FeatureResourceDecorator(final FeatureManager manager) { + this.manager = manager; + } @Override public Resource decorate(final Resource resource) { + Resource result = resource; final ClientContext info = manager.getCurrentClientContext(); - if ( info != null ) { - final String resourceType = resource.getResourceType(); - final String overwriteType = ((ClientContextImpl)info).getResourceTypeMapping().get(resourceType); - if ( overwriteType != null ) { - return new ResourceWrapper(resource) { - - @Override - public String getResourceType() { - return overwriteType; - } - - @Override - public String getResourceSuperType() { - return resourceType; - } - }; + if (info instanceof ClientContextImpl) { + for (ResourceDecorator rd : ((ClientContextImpl) info).getResourceDecorators()) { + Resource r = rd.decorate(resource); + if (r != null) { + result = r; + } } } - return resource; + return result; } @Override diff --git a/src/main/java/org/apache/sling/featureflags/impl/FeatureWebConsolePlugin.java b/src/main/java/org/apache/sling/featureflags/impl/FeatureWebConsolePlugin.java new file mode 100644 index 0000000..ff2b8d7 --- /dev/null +++ b/src/main/java/org/apache/sling/featureflags/impl/FeatureWebConsolePlugin.java @@ -0,0 +1,74 @@ +/* + * 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 java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.featureflags.ExecutionContext; +import org.apache.sling.featureflags.Feature; + +@SuppressWarnings("serial") +public class FeatureWebConsolePlugin extends HttpServlet { + + private final FeatureManager featureManager; + + FeatureWebConsolePlugin(final FeatureManager featureManager) { + this.featureManager = featureManager; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + final PrintWriter pw = resp.getWriter(); + final Feature[] features = this.featureManager.getAvailableFeatures(); + if (features == null || features.length == 0) { + 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); + pw.println("<table class='nicetable'>"); + pw.println("<tr><th>Name</th><th>Description</th><th>Enabled</th></tr>"); + final ExecutionContext ctx = createContext(req); + for (final Feature feature : features) { + pw.printf("<tr><td>%s</td><td>%s</td><td>%s</td></tr>%n", feature.getName(), feature.getDescription(), + feature.isEnabled(ctx)); + } + pw.println("</table>"); + } + } + + private ExecutionContext createContext(final HttpServletRequest req) { + return new ExecutionContext() { + + @Override + public ResourceResolver getResourceResolver() { + return null; + } + + @Override + public HttpServletRequest getRequest() { + return req; + } + }; + } +} diff --git a/src/main/java/org/apache/sling/featureflags/impl/FeaturesImpl.java b/src/main/java/org/apache/sling/featureflags/impl/FeaturesImpl.java index e0e836e..eb67dd6 100644 --- a/src/main/java/org/apache/sling/featureflags/impl/FeaturesImpl.java +++ b/src/main/java/org/apache/sling/featureflags/impl/FeaturesImpl.java @@ -18,10 +18,8 @@ */ package org.apache.sling.featureflags.impl; -import org.apache.felix.scr.annotations.Component; -import org.apache.felix.scr.annotations.Reference; -import org.apache.felix.scr.annotations.Service; -import org.apache.sling.api.SlingHttpServletRequest; +import javax.servlet.http.HttpServletRequest; + import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.featureflags.ClientContext; import org.apache.sling.featureflags.Feature; @@ -30,12 +28,13 @@ import org.apache.sling.featureflags.Features; /** * This is a wrapper around the internal feature manager. */ -@Component -@Service(value=Features.class) public class FeaturesImpl implements Features { - @Reference - private FeatureManager manager; + private final FeatureManager manager; + + FeaturesImpl(final FeatureManager manager) { + this.manager = manager; + } @Override public String[] getAvailableFeatureNames() { @@ -68,7 +67,7 @@ public class FeaturesImpl implements Features { } @Override - public ClientContext createClientContext(final SlingHttpServletRequest request) { + public ClientContext createClientContext(final HttpServletRequest request) { return this.manager.createClientContext(request); } } diff --git a/src/main/java/org/apache/sling/featureflags/impl/ResourceAccessImpl.java b/src/main/java/org/apache/sling/featureflags/impl/ResourceAccessImpl.java deleted file mode 100644 index 8772cf3..0000000 --- a/src/main/java/org/apache/sling/featureflags/impl/ResourceAccessImpl.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 org.apache.felix.scr.annotations.Component; -import org.apache.felix.scr.annotations.Property; -import org.apache.felix.scr.annotations.Reference; -import org.apache.felix.scr.annotations.Service; -import org.apache.sling.api.resource.Resource; -import org.apache.sling.featureflags.ClientContext; -import org.apache.sling.featureflags.ResourceHiding; -import org.apache.sling.resourceaccesssecurity.AllowingResourceAccessGate; -import org.apache.sling.resourceaccesssecurity.ResourceAccessGate; - -/** - * Resource access gate implementing the hiding of resources. - */ -@Component -@Service(value=ResourceAccessGate.class) -@Property(name=ResourceAccessGate.CONTEXT, value=ResourceAccessGate.APPLICATION_CONTEXT) -public class ResourceAccessImpl - extends AllowingResourceAccessGate - implements ResourceAccessGate { - - @Reference - private FeatureManager manager; - - @Override - public GateResult canRead(final Resource resource) { - boolean available = true; - final ClientContext info = manager.getCurrentClientContext(); - if ( info != null ) { - for(final ResourceHiding f : ((ClientContextImpl)info).getHidingFeatures() ) { - available = !f.hideResource(resource); - if ( !available) { - break; - } - } - } - return (available ? GateResult.DONTCARE : GateResult.DENIED); - } -} 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 90264d0..f1fb45b 100644 --- a/src/main/java/org/apache/sling/featureflags/package-info.java +++ b/src/main/java/org/apache/sling/featureflags/package-info.java @@ -18,10 +18,48 @@ */ /** - * Provides a service to interface which may be implemented by applications - * to get notified on cluster topology changes. + * The <i>Feature Flags</i> feature allows applications to dynamically + * provide features to clients and consumers depending on various criteria such as + * <ul> + * <li>Time of Day</li> + * <li>Static Configuration</li> + * <li>Request Parameter</li> + * <li>Some Resource</li> + * </ul> + * <p> + * Feature flag support consists of two parts: The feature flag itself represented + * by the {@link org.apache.sling.featureflags.Feature Feature} interface and the + * the application providing a feature guarded by a feature flag. Such applications + * make use of the {@link org.apache.sling.featureflags.Features Features} service to + * query feature flags. + * <p> + * Feature flags can be provided by registering + * {@link org.apache.sling.featureflags.Feature Feature} services. Alternatively + * feature flags can be provided by factory configuration with factory PID + * {@code org.apache.sling.featureflags.Feature} as follows: + * <table> + * <tr> + * <td>{@code name}</td> + * <td>Short name of this feature. This name is used to refer to this feature + * when checking for it to be enabled or not. This property is required + * and defaults to a name derived from the feature's class name and object + * identity. It is strongly recommended to define a useful and unique for the feature</td> + * </tr> + * <tr> + * <td>{@code description}</td> + * <td>Description for the feature. The intent is to descibe the behaviour + * of the application if this feature would be enabled. It is recommended + * to define this property. The default value is the value of the name property.</td> + * </tr> + * <tr> + * <td>{@code enabled}</td> + * <td>Boolean flag indicating whether the feature is enabled or not by + * this configuration</td> + * </tr> + * </table> * * @version 1.0 + * @see <a href="http://sling.apache.org/documentation/the-sling-engine/featureflags.html">Feature Flags</a> */ @Version("1.0") package org.apache.sling.featureflags; -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
