blob: 17464fdf4c5feb9f02673c8136487818e8901403 [file] [log] [blame]
/*
* 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 java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.request.ResponseUtil;
import org.apache.sling.featureflags.Feature;
import org.apache.sling.featureflags.Features;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
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.http.whiteboard.HttpWhiteboardConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service implements the feature handling. It keeps track of all
* {@link Feature} services.
*/
@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"
})
public class FeatureManager implements Features, Filter, Servlet {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final ThreadLocal<ExecutionContextImpl> perThreadClientContext = new ThreadLocal<ExecutionContextImpl>();
private final Map<String, List<FeatureDescription>> allFeatures = new HashMap<String, List<FeatureDescription>>();
private Map<String, Feature> activeFeatures = Collections.emptyMap();
private ServletConfig servletConfig;
//--- Features
@Override
public Feature[] getFeatures() {
final Map<String, Feature> activeFeatures = this.activeFeatures;
return activeFeatures.values().toArray(new Feature[activeFeatures.size()]);
}
@Override
public Feature getFeature(final String name) {
return this.activeFeatures.get(name);
}
@Override
public boolean isEnabled(final String featureName) {
final Feature feature = this.getFeature(featureName);
if (feature != null) {
return getCurrentExecutionContext().isEnabled(feature);
}
return false;
}
//--- Filter
@Override
public void init(final FilterConfig filterConfig) {
// nothing to do
}
@Override
public void doFilter(final ServletRequest request,
final ServletResponse response,
final FilterChain chain)
throws IOException, ServletException {
this.pushContext((HttpServletRequest) request);
try {
chain.doFilter(request, response);
} finally {
this.popContext();
}
}
@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) {
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 ExecutionContextImpl ctx = getCurrentExecutionContext();
for (final Feature feature : features) {
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));
}
pw.println("</table>");
}
} else {
((HttpServletResponse) res).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
res.flushBuffer();
}
}
//--- Feature binding
// 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();
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);
}
candidates.add(info);
Collections.sort(candidates);
this.calculateActiveProviders();
}
}
// 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
candidates.remove(info);
if (candidates.size() == 0) {
this.allFeatures.remove(name);
}
}
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) {
this.perThreadClientContext.set(new ExecutionContextImpl(this, request));
}
void popContext() {
this.perThreadClientContext.set(null);
}
ExecutionContextImpl getCurrentExecutionContext() {
ExecutionContextImpl ctx = this.perThreadClientContext.get();
return (ctx != null) ? ctx : new ExecutionContextImpl(this, null);
}
/**
* 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) {
this.feature = feature;
final Object sr = props.get(Constants.SERVICE_RANKING);
if (sr instanceof Integer) {
this.ranking = (Integer) sr;
} else {
this.ranking = 0;
}
this.serviceId = (Long) props.get(Constants.SERVICE_ID);
}
@Override
public int compareTo(final FeatureDescription o) {
if (this.ranking < o.ranking) {
return 1;
} else if (this.ranking > o.ranking) {
return -1;
}
// If ranks are equal, then sort by service id in descending order.
return (this.serviceId < o.serviceId) ? -1 : 1;
}
@Override
public boolean equals(final Object obj) {
if (obj instanceof FeatureDescription) {
return ((FeatureDescription) obj).serviceId == this.serviceId;
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (serviceId ^ (serviceId >>> 32));
return result;
}
}
}