/* * 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.launchpad.base.webapp; import static org.apache.felix.framework.util.FelixConstants.LOG_LEVEL_PROP; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.servlet.GenericServlet; import javax.servlet.Servlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import org.apache.felix.framework.Logger; import org.apache.felix.http.proxy.ProxyServlet; import org.apache.sling.launchpad.api.LaunchpadContentProvider; import org.apache.sling.launchpad.base.impl.ClassLoaderResourceProvider; import org.apache.sling.launchpad.base.impl.Sling; import org.apache.sling.launchpad.base.shared.Launcher; import org.apache.sling.launchpad.base.shared.Notifiable; import org.apache.sling.launchpad.base.shared.SharedConstants; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.osgi.framework.ServiceEvent; import org.osgi.framework.ServiceReference; /** * The <code>SlingServletDelegate</code> serves as a basic servlet for Project Sling. * The tasks of this servlet are as follows: * <ul> * <li>The {@link #init()} method launches Apache <code>Felix</code> as the * OSGi framework implementation we use. * <li>Registers as a service listener interested for services of type * <code>javax.servlet.Servlet</code>. * <li>Handles requests by delegating to a servlet which is expected to be * registered with the framework as a service of type * <code>javax.servlet.Servlet</code>. If no delegatee servlet has been * registered request handlings results in a temporary unavailability of the * servlet. * </ul> * <p> * <b>Request Handling</b> * <p> * This servlet handles request by forwarding to a delegatee servlet. The * delegatee servlet is automatically retrieved from the service registry by the * {@link #getDelegatee()}. This method also makes sure, the such a servlet * actually exits by throwing an <code>UnvailableException</code> if not and * also makes sure the servlet is initialized. * <p> * <b>Launch Configuration</b> * <p> * The Apache <code>Felix</code> framework requires configuration parameters * to be specified for startup. This servlet builds the list of parameters from * three locations: * <ol> * <li>The <code>sling.properties</code> is read * from the servlet class path. This properties file contains default settings.</li> * <li>Extensions of this servlet may provide additional properties to be * loaded overwriting the {@link #loadConfigProperties(String)} method. * <li>Finally, web application init parameters are added to the properties and * may overwrite existing properties of the same name(s). * </ol> * <p> * After loading all properties, variable substitution takes place on the * property values. A variable is indicated as <code>${<prop-name>}</code> * where <code><prop-name></code> is the name of a system or * configuration property (configuration properties override system properties). * Variables may be nested and are resolved from inner-most to outer-most. For * example, the property value <code>${outer-${inner}}</code> is resolved by * first resolving <code>${inner}</code> and then resolving the property whose * name is the catenation of <code>outer-</code> and the result of resolving * <code>${inner}</code>. * <p> * <b>Logging</b> * <p> * This servlet logs through the servlet container logging mechanism by calling * the <code>GenericServlet.log</code> methods. Bundles launched within the * framework provided by this servlet may use whatever logging mechanism they * choose to use. The Commons OSGI Log Bundle provides an OSGi Log Service * implementation, which also provides access to Apache Commons Logging, SLF4J * and Log4J logging. It is recommended that this bundle is used to setup and * configure logging for systems based on this servlet. */ public class SlingServletDelegate extends GenericServlet implements Launcher { /** Pseduo class version ID to keep the IDE quite. */ private static final long serialVersionUID = 1L; /** Mapping between log level numbers and names */ private static final String[] logLevels = { "FATAL", "ERROR", "WARN", "INFO", "DEBUG" }; /** * The Sling configuration property name setting the initial log level * (corresponds to LogbackManager.LOG_LEVEL constant) */ private static final String PROP_LOG_LEVEL = "org.apache.sling.commons.log.level"; /** * The name of the configuration property defining the obr repository. */ private static final String OBR_REPOSITORY_URL = "obr.repository.url"; /** * Flag set by the {@link #destroy()} method to indicate the servlet has * been destroyed. This flag is used by the {@link #startSling(String)} * method to check whether the SlingServletDelegate has been destroyed while Sling * was starting up. */ private boolean servletDestroyed = false; /** * The OSGI framework instance loaded on {@link #init()} and stopped * on {@link #destroy()}. */ private Sling sling; /** * The map of delegatee servlets to which requests are delegated. This map * is managed through the * {@link #serviceChanged(ServiceEvent) service listener} based on servlets * registered. * * @see #getDelegatee() * @see #ungetDelegatee(Object) */ private Servlet delegatee; private Notifiable notifiable; private Map<String, String> properties; private String slingHome; @Override public void setNotifiable(Notifiable notifiable) { this.notifiable = notifiable; } @Override public void setCommandLine(Map<String, String> args) { this.properties = args; } @Override public void setSlingHome(String slingHome) { this.slingHome = slingHome; } @Override public boolean start() { // might want to log, why we don't start ! return false; } @Override public void stop() { destroy(); } /** * Initializes this servlet by loading the framework configuration * properties, starting the OSGi framework (Apache Felix) and exposing the * system bundle context and the <code>Felix</code> instance as servlet * context attributes. * * @throws ServletException if the framework cannot be initialized. */ @Override public final void init() throws ServletException { // temporary holders control final setup and ensure proper // disposal in case of setup errors Sling tmpSling = null; Servlet tmpDelegatee = null; try { log("Starting Apache Sling in " + slingHome); // read the default parameters Map<String, String> props = loadConfigProperties(slingHome); Logger logger = new ServletContextLogger(getServletContext()); LaunchpadContentProvider rp = new ServletContextResourceProvider( getServletContext()); tmpSling = SlingBridge.getSlingBridge(notifiable, logger, rp, props, getServletContext()); // set up the OSGi HttpService proxy servlet tmpDelegatee = new ProxyServlet(); tmpDelegatee.init(getServletConfig()); // set the fields only if the SlingServletDelegate has no been destroyed // while Sling has been starting up. Otherwise we do not set the // fields and leave the temporary variables assigned to have // them destroyed in the finally clause. if (servletDestroyed) { log("SlingServletDelegate destroyed while starting Apache Sling, shutting Apache Sling down"); } else { // set the fields now sling = tmpSling; delegatee = tmpDelegatee; // reset temporary holders to prevent destroyal tmpSling = null; tmpDelegatee = null; log("Apache Sling successfully started in " + slingHome); } } catch (BundleException be) { throw new ServletException("Failed to start Apache Sling in " + slingHome, be); } catch (ServletException se) { throw new ServletException("Failed to start bridge servlet for Apache Sling", se); } catch (Throwable t) { throw new ServletException("Uncaught Failure starting Apache Sling", t); } finally { // clean up temporary fields if (tmpDelegatee != null) { tmpDelegatee.destroy(); } if (tmpSling != null) { tmpSling.destroy(); } } } /** * Services the request by delegating to the delegatee servlet. If no * delegatee servlet is available, a <code>UnavailableException</code> is * thrown. * * @param req the <code>ServletRequest</code> object that contains the * client's request * @param res the <code>ServletResponse</code> object that will contain * the servlet's response * @throws javax.servlet.UnavailableException if the no delegatee servlet is currently * available * @throws ServletException if an exception occurs that interferes with the * servlet's normal operation occurred * @throws IOException if an input or output exception occurs */ @Override public final void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { // delegate the request to the registered delegatee servlet Servlet delegatee = getDelegatee(); if (delegatee == null) { ((HttpServletResponse) res).sendError(HttpServletResponse.SC_NOT_FOUND); } else { delegatee.service(req, res); } } /** * Destroys this servlet by shutting down the OSGi framework and hence the * delegatee servlet if one is set at all. */ @Override public final void destroy() { // set the destroyed flag to signal to the startSling method // that Sling should be terminated immediately servletDestroyed = true; // destroy the delegatee if (delegatee != null) { delegatee.destroy(); delegatee = null; } // shutdown the Felix container if (sling != null) { sling.destroy(); sling = null; } // finally call the base class destroy method super.destroy(); } Servlet getDelegatee() { return delegatee; } // ---------- Configuration Loading ---------------------------------------- /** * Loads the configuration properties in the configuration property file * associated with the framework installation; these properties are * accessible to the framework and to bundles and are intended for * configuration purposes. By default, the configuration property file is * located in the <tt>conf/</tt> directory of the Felix installation * directory and is called "<tt>config.properties</tt>". The * installation directory of Felix is assumed to be the parent directory of * the <tt>felix.jar</tt> file as found on the system class path property. * The precise file from which to load configuration properties can be set * by initializing the "<tt>felix.config.properties</tt>" system * property to an arbitrary URL. * * @param slingHome The value to be used as the "sling.home" property in the * returned map. This parameter is expected to be non-<code>null</code>. * @return A <tt>Properties</tt> instance. */ private Map<String, String> loadConfigProperties(String slingHome) { // The config properties file is either specified by a system // property or it is in the same directory as the Felix JAR file. // Try to load it from one of these places. Map<String, String> props = new HashMap<String, String>(); // The following property must start with a comma! final String servletVersion = getServletContext().getMajorVersion() + "." + getServletContext().getMinorVersion(); String packages = ",javax.servlet;javax.servlet.http;javax.servlet.resources"; if ( getServletContext().getMajorVersion() >= 3 ) { // servlet 3.x adds new packages and we should export as 2.6 and 3.x packages = packages + "; version=2.6" + packages + ";javax.servlet.annotation;javax.servlet.descriptor"; } props.put( Sling.PROP_SYSTEM_PACKAGES, packages + "; version=" + servletVersion); // extra capabilities final String servletCaps = "osgi.contract;osgi.contract=JavaServlet;version:Version=\" " + servletVersion + "\";" + "uses:=\"javax.servlet,javax.servlet.http,javax.servlet.descriptor,javax.servlet.annotation\""; props.put(Sling.PROP_EXTRA_CAPS, servletCaps); // prevent system properties from being considered props.put(Sling.SLING_IGNORE_SYSTEM_PROPERTIES, "true"); if (this.properties != null) { props.putAll(this.properties); } else { // copy context init parameters Enumeration<String> cpe = getServletContext().getInitParameterNames(); while (cpe.hasMoreElements()) { String name = cpe.nextElement(); props.put(name, getServletContext().getInitParameter(name)); } // copy servlet init parameters Enumeration<String> pe = getInitParameterNames(); while (pe.hasMoreElements()) { String name = pe.nextElement(); props.put(name, getInitParameter(name)); } } // ensure the Felix Logger loglevel matches the Sling log level checkLogSettings(props); // if the specified obr location is not a url and starts with a '/', we // assume that this location is inside the webapp and create the correct // full url final String repoLocation = props.get(OBR_REPOSITORY_URL); if (insideWebapp(repoLocation)) { final URL url = getUrl(repoLocation); // only if we get back a resource url, we update it if (url != null) props.put(OBR_REPOSITORY_URL, url.toExternalForm()); } // set sling home props.put(SharedConstants.SLING_HOME, slingHome); // ensure sling.launchpad is set if (!props.containsKey(SharedConstants.SLING_LAUNCHPAD)) { props.put(SharedConstants.SLING_LAUNCHPAD, slingHome); } return props; } private void checkLogSettings(Map<String, String> props) { String logLevelString = props.get(PROP_LOG_LEVEL); if (logLevelString != null) { int logLevel = 1; try { logLevel = Integer.parseInt(logLevelString); } catch (NumberFormatException nfe) { // might be a loglevel name for (int i=0; i < logLevels.length; i++) { if (logLevels[i].equalsIgnoreCase(logLevelString)) { logLevel = i; break; } } } props.put(LOG_LEVEL_PROP, String.valueOf(logLevel)); } } private boolean insideWebapp(String path) { return path != null && path.indexOf(":/") < 1 && path.startsWith("/"); } private URL getUrl(String path) { try { return getServletContext().getResource(path); } catch (MalformedURLException e) { return null; } } private static class ServletContextLogger extends Logger { private ServletContext servletContext; private ServletContextLogger(ServletContext servletContext) { this.servletContext = servletContext; } @Override protected void doLog( Bundle bundle, @SuppressWarnings("rawtypes") ServiceReference sr, int level, String msg, Throwable throwable) { // unwind throwable if it is a BundleException if ((throwable instanceof BundleException) && (((BundleException) throwable).getNestedException() != null)) { throwable = ((BundleException) throwable).getNestedException(); } String s = (sr == null) ? null : "SvcRef " + sr; s = (s == null) ? null : s + " Bundle '" + bundle.getBundleId() + "'"; s = (s == null) ? msg : s + " " + msg; s = (throwable == null) ? s : s + " (" + throwable + ")"; switch (level) { case LOG_DEBUG: servletContext.log("DEBUG: " + s); break; case LOG_ERROR: if (throwable == null) { servletContext.log("ERROR: " + s); } else { servletContext.log("ERROR: " + s, throwable); } break; case LOG_INFO: servletContext.log("INFO: " + s); break; case LOG_WARNING: servletContext.log("WARNING: " + s); break; default: servletContext.log("UNKNOWN[" + level + "]: " + s); } } } private static class ServletContextResourceProvider extends ClassLoaderResourceProvider { /** * The root folder for internal web application files (value is * "/WEB-INF/"). */ private static final String WEB_INF = "/WEB-INF"; private ServletContext servletContext; private ServletContextResourceProvider(ServletContext servletContext) { super(SlingServletDelegate.class.getClassLoader()); this.servletContext = servletContext; } @SuppressWarnings("unchecked") @Override public Iterator<String> getChildren(String path) { // ensure leading slash if (path.charAt(0) != '/') { path = "/" + path; } @SuppressWarnings("rawtypes") Set resources = servletContext.getResourcePaths(path); // unchecked if (resources == null || resources.isEmpty()) { resources = servletContext.getResourcePaths(WEB_INF + path); // unchecked } @SuppressWarnings("rawtypes") Iterator resourceIterator; if ( resources == null || resources.isEmpty() ) { // fall back to the class path resourceIterator = super.getChildren(path); if(resourceIterator.hasNext()) { return resourceIterator; } // fall back to WEB-INF within the class path resourceIterator = super.getChildren(WEB_INF + path); if(resourceIterator.hasNext()) { return resourceIterator; } } if ( resources == null ) { return Collections.EMPTY_LIST.iterator(); } return resources.iterator(); // unchecked } @Override public URL getResource(String path) { // nothing for empty or null path if (path == null || path.length() == 0) { return null; } // ensure leading slash if (path.charAt(0) != '/') { path = "/" + path; } try { // try direct path URL resource = servletContext.getResource(path); if (resource != null) { return resource; } // otherwise try WEB-INF location resource = servletContext.getResource(WEB_INF + path); if(resource != null) { return resource; } // try classpath resource = super.getResource(path); if(resource != null) { return resource; } // try WEB-INF within the classpath resource = super.getResource(WEB_INF + path); if(resource != null) { return resource; } } catch (MalformedURLException mue) { servletContext.log("Failure to get resource " + path, mue); } // fall back to no resource found return null; } } }