/* * 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; } } }