| /* |
| * 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.webapp; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import javax.servlet.GenericServlet; |
| import javax.servlet.Servlet; |
| import javax.servlet.ServletException; |
| import javax.servlet.ServletRequest; |
| import javax.servlet.ServletResponse; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletRequestWrapper; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.sling.launchpad.base.shared.Launcher; |
| import org.apache.sling.launchpad.base.shared.Loader; |
| import org.apache.sling.launchpad.base.shared.Notifiable; |
| import org.apache.sling.launchpad.base.shared.SharedConstants; |
| import org.apache.sling.launchpad.base.shared.Util; |
| |
| /** |
| * The <code>SlingServlet</code> is the externally visible Web Application |
| * launcher for Sling. Please refer to the full description <i>The Sling |
| * Launchpad</i> on the Sling Wiki for a full description of this class. |
| * <p> |
| * Logging goes to ServletContext.log methods. |
| * <p> |
| * This class goes into the secondary artifact with the classifier <i>webapp</i> |
| * to be used as the main servlet to be registered in the servlet container. |
| * |
| * @see <a href="http://cwiki.apache.org/SLING/the-sling-launchpad.html">The |
| * Sling Launchpad</a> |
| */ |
| @SuppressWarnings("serial") |
| public class SlingServlet extends GenericServlet implements Notifiable { |
| |
| /** |
| * The number times Sling will be tried to be started before giving up |
| * (value is 20). This number is chosen deliberately as generally Sling |
| * should start up smoothly. Whether any bundles within Sling start or not |
| * is not counted here. |
| */ |
| private static final int MAX_START_FAILURES = 20; |
| |
| /** |
| * The name of the system property which may be set to define the default |
| * prefix for the sling.home value generated from the Sling servlet context |
| * path. |
| * |
| * @see #toSlingHome(String) |
| */ |
| private static final String SLING_HOME_PREFIX = "sling.home.prefix"; |
| |
| /** |
| * The default value to be used as the prefix for the sling.home value |
| * generated from the Sling servlet context path if the |
| * {@link #SLING_HOME_PREFIX sling.home.prefix} system property is not set. |
| * |
| * @see #toSlingHome(String) |
| */ |
| private static final String SLING_HOME_PREFIX_DEFAULT = "sling/"; |
| |
| private Map<String, String> properties; |
| |
| private String slingHome; |
| |
| private Loader loader; |
| |
| private Servlet sling; |
| |
| /** |
| * Field managed by the {@link #startSling(String)} method to indicate |
| * whether Sling is in the process of being started. |
| */ |
| private Thread startingSling; |
| |
| /** |
| * Counter to count the number of failed startups. After this number |
| * expires, the SlingServlet will not try to start Sling any more. |
| */ |
| private int startFailureCounter = 0; |
| |
| // ---------- GenericServlet |
| |
| /** |
| * Launches the SLing framework if the sling.home setting can be derived |
| * from the configuration or the SerlvetContext. Otherwise Sling is not |
| * started yet and will be started when the first request comes in. |
| */ |
| @Override |
| public void init() { |
| this.properties = collectInitParameters(); |
| |
| this.slingHome = getSlingHome(null); |
| if (this.slingHome != null) { |
| startSling(); |
| } else { |
| log("Apache Sling cannot be started yet, because sling.home is not defined yet"); |
| } |
| |
| log("Servlet " + getServletName() + " initialized"); |
| } |
| |
| @Override |
| public String getServletInfo() { |
| if (sling != null) { |
| return sling.getServletInfo(); |
| } |
| |
| return "Sling Launchpad Proxy"; |
| } |
| |
| /** |
| * If Sling has already been started, the request is forwarded to the |
| * started Sling framework. Otherwise the Sling framework is started unless |
| * there were too many startup failures. |
| * <p> |
| * If the request is not forwarded to Sling, this method returns a 404/NOT |
| * FOUND if the startup failure counter has exceeded or 503/SERVICE |
| * UNAVAILABLE if the Sling framework is starting up. |
| * <p> |
| * If a request causes the framework to start, it is immediately terminated |
| * with said response status and framework is started in a separate thread. |
| */ |
| @Override |
| public void service(ServletRequest req, ServletResponse res) |
| throws ServletException, IOException { |
| |
| // delegate the request to the registered delegatee servlet |
| Servlet delegatee = sling; |
| if (delegatee != null) { |
| |
| // check for problematic application servers like WebSphere |
| // where path info and servlet path is set wrong SLING-2410 |
| final HttpServletRequest request = (HttpServletRequest) req; |
| if ( request.getPathInfo() == null && request.getServletPath() != null |
| && request.getServletPath().endsWith(".jsp") ) { |
| req = new HttpServletRequestWrapper(request) { |
| |
| @Override |
| public String getPathInfo() { |
| return request.getServletPath(); |
| } |
| |
| @Override |
| public String getServletPath() { |
| return ""; |
| } |
| |
| }; |
| } |
| delegatee.service(req, res); |
| |
| } else if (startFailureCounter > MAX_START_FAILURES) { |
| |
| // too many startup retries, fail for ever |
| ((HttpServletResponse) res).sendError(HttpServletResponse.SC_NOT_FOUND); |
| |
| } else { |
| |
| startSling(req); |
| |
| ((HttpServletResponse) res).sendError( |
| HttpServletResponse.SC_SERVICE_UNAVAILABLE, |
| "Apache Sling is currently starting up, please try again"); |
| } |
| } |
| |
| /** |
| * Stop the Sling framework when the web application is being stopped |
| */ |
| @Override |
| public void destroy() { |
| if (sling != null) { |
| sling.destroy(); |
| } |
| |
| // clear fields |
| slingHome = null; |
| loader = null; |
| sling = null; |
| } |
| |
| // ---------- Notifiable interface |
| |
| /** |
| * The framework has been stopped by calling the <code>Bundle.stop()</code> |
| * on the system bundle. This actually terminates the Sling Standalone |
| * application. |
| * <p> |
| * Note, that a new request coming in while the web application is still |
| * running, will actually cause Sling to restart ! |
| */ |
| @Override |
| public void stopped() { |
| /** |
| * This method is called if the framework is stopped from within by |
| * calling stop on the system bundle or if the framework is stopped |
| * because the VM is going down and the shutdown hook has initated the |
| * shutdown In any case we ensure the reference to the framework is |
| * removed and remove the shutdown hook (but don't care if that fails). |
| */ |
| |
| log("Apache Sling has been stopped"); |
| |
| // clear the reference to the framework |
| sling = null; |
| } |
| |
| /** |
| * The framework has been stopped with the intent to be restarted by calling |
| * either of the <code>Bundle.update</code> methods on the system bundle. |
| * <p> |
| * If an <code>InputStream</code> was provided, this has been copied to a |
| * temporary file, which will be used in place of the existing launcher jar |
| * file. |
| * |
| * @param updateFile The temporary file to replace the existing launcher jar |
| * file. If <code>null</code> the existing launcher jar will be |
| * used again. |
| */ |
| @Override |
| public void updated(File updateFile) { |
| |
| // drop the sling reference to be able to restart |
| synchronized (this) { |
| if (startingSling == null) { |
| sling = null; |
| } |
| } |
| |
| // ensure we have a VM as clean as possible |
| loader.cleanupVM(); |
| |
| if (updateFile == null) { |
| |
| log("Restarting Framework and Apache Sling"); |
| startSling((URL) null); |
| |
| } else { |
| |
| log("Restarting Framework with update from " + updateFile); |
| try { |
| startSling(updateFile.toURI().toURL()); |
| } catch (MalformedURLException mue) { |
| log("Cannot get URL for file " + updateFile, mue); |
| } finally { |
| updateFile.delete(); |
| } |
| |
| } |
| } |
| |
| // --------- internal |
| |
| /** |
| * If Sling is not currently starting up, a thread is started to start Sling |
| * in the background. |
| */ |
| private void startSling(final ServletRequest request) { |
| if (startingSling == null) { |
| slingHome = getSlingHome((HttpServletRequest) request); |
| Thread starter = new Thread(new Runnable() { |
| @Override |
| public void run() { |
| startSling(); |
| } |
| }, "SlingStarter_" + System.currentTimeMillis()); |
| |
| starter.setDaemon(true); |
| starter.start(); |
| } |
| } |
| |
| /** |
| * Called from the startup thread initiated by a request or from |
| * {@link #init()} to install the launcher jar and actually start sling. |
| */ |
| private void startSling() { |
| |
| try { |
| File launchpadHome = getLaunchpadHome(slingHome); |
| this.loader = new Loader(launchpadHome) { |
| @Override |
| protected void info(String msg) { |
| log(msg); |
| } |
| }; |
| } catch (IllegalArgumentException iae) { |
| startupFailure(null, iae); |
| return; |
| } |
| |
| try { |
| URL launcherJar = getServletContext().getResource( |
| SharedConstants.DEFAULT_SLING_LAUNCHER_JAR); |
| if (launcherJar == null) { |
| launcherJar = getServletContext().getResource( |
| "/WEB-INF" + SharedConstants.DEFAULT_SLING_LAUNCHER_JAR); |
| } |
| |
| startSling(launcherJar); |
| } catch (MalformedURLException mue) { |
| log("Cannot load Apache Sling Launcher JAR " |
| + SharedConstants.DEFAULT_SLING_LAUNCHER_JAR, mue); |
| } |
| } |
| |
| /** |
| * Installs the launcher jar from the given URL (if not <code>null</code>) |
| * and launches Sling from that launcher. |
| */ |
| private void startSling(URL launcherJar) { |
| synchronized (this) { |
| if (sling != null) { |
| log("Apache Sling already started, nothing to do"); |
| return; |
| } else if (startingSling != null) { |
| log("Apache Sling being started by Thread " + startingSling); |
| return; |
| } |
| |
| startingSling = Thread.currentThread(); |
| } |
| |
| if (launcherJar != null) { |
| try { |
| log("Checking launcher JAR in " + slingHome); |
| loader.installLauncherJar(launcherJar); |
| } catch (IOException ioe) { |
| startupFailure("Failed installing " + launcherJar, ioe); |
| return; |
| } |
| } else { |
| log("No Launcher JAR to install"); |
| } |
| |
| Object object = null; |
| try { |
| object = loader.loadLauncher(SharedConstants.DEFAULT_SLING_SERVLET); |
| } catch (IllegalArgumentException iae) { |
| startupFailure("Cannot load Launcher Servlet " |
| + SharedConstants.DEFAULT_SLING_SERVLET, iae); |
| return; |
| } |
| |
| if (object instanceof Servlet) { |
| Servlet sling = (Servlet) object; |
| |
| if (sling instanceof Launcher) { |
| Launcher slingLauncher = (Launcher) sling; |
| slingLauncher.setNotifiable(this); |
| slingLauncher.setCommandLine(properties); |
| slingLauncher.setSlingHome(slingHome); |
| } |
| |
| try { |
| log("Starting launcher ..."); |
| sling.init(getServletConfig()); |
| this.sling = sling; |
| this.startFailureCounter = 0; |
| log("Startup completed"); |
| } catch (ServletException se) { |
| startupFailure(null, se); |
| } |
| } |
| |
| // reset the starting flag |
| synchronized (this) { |
| startingSling = null; |
| } |
| } |
| |
| /** |
| * Define the sling.home parameter implementing the algorithme defined on |
| * the wiki page to find the setting according to this algorithm: |
| * <ol> |
| * <li>Servlet parameter <code>sling.home</code></li> |
| * <li>Context <code>sling.home</code></li> |
| * <li>Derived from ServletContext path</li> |
| * </ol> |
| * <p> |
| * <code>null</code> may be returned by this method if no |
| * <code>sling.home</code> parameter is set and if the servlet container |
| * does not provide the Servlet API 2.5 |
| * <code>ServletContext.getContextPath()</code> method and the |
| * <code>request</code> parameter is <code>null</code>. |
| * <p> |
| * If <code>sling.home</code> can be retrieved, it is returned as an |
| * absolute path. |
| * |
| * @param args The command line arguments |
| * @return The value to use for sling.home or <code>null</code> if the value |
| * cannot be retrieved. |
| */ |
| private String getSlingHome(HttpServletRequest request) { |
| |
| String source = null; |
| |
| // access config and context to be able to log the sling.home source |
| |
| // 1. servlet config parameter |
| String slingHome = getServletConfig().getInitParameter( |
| SharedConstants.SLING_HOME); |
| if (slingHome != null) { |
| |
| source = "servlet parameter sling.home"; |
| |
| } else { |
| |
| // 2. servlet context parameter |
| slingHome = getServletContext().getInitParameter( |
| SharedConstants.SLING_HOME); |
| if (slingHome != null) { |
| |
| source = "servlet context parameter sling.home"; |
| |
| } else { |
| |
| // 3. servlet context path (Servlet API 2.5 and later) |
| try { |
| |
| String contextPath = getServletContext().getContextPath(); |
| slingHome = toSlingHome(contextPath); |
| source = "servlet context path"; |
| |
| } catch (NoSuchMethodError nsme) { |
| |
| // 4.servlet context path (Servlet API 2.4 and earlier) |
| if (request != null) { |
| |
| String contextPath = request.getContextPath(); |
| slingHome = toSlingHome(contextPath); |
| source = "servlet context path (from request)"; |
| |
| } else { |
| |
| log("ServletContext path not available here, delaying startup until first request"); |
| return null; |
| |
| } |
| } |
| |
| } |
| } |
| |
| // substitute any ${...} references and make absolute |
| slingHome = Util.substVars(slingHome, SharedConstants.SLING_HOME, null, properties); |
| slingHome = new File(slingHome).getAbsolutePath(); |
| |
| log("Setting sling.home=" + slingHome + " (" + source + ")"); |
| return slingHome; |
| } |
| |
| /** |
| * Define the sling.launchpad parameter implementing the algorithme defined |
| * on the wiki page to find the setting according to this algorithm: |
| * <ol> |
| * <li>Servlet init parameter <code>sling.launchpad</code>. This path is |
| * resolved against the <code>slingHome</code> folder if relative.</li> |
| * <li>Servlet context init parameter <code>sling.launchpad</code>. This |
| * path is resolved against the <code>slingHome</code> folder if relative.</li> |
| * <li>Default to same as <code>sling.home</code></li> |
| * </ol> |
| * <p> |
| * The absolute path of the returned file is stored as the |
| * <code>sling.launchpad</code> property in the {@link #properties} map. |
| * |
| * @param slingHome The absolute path to the Sling Home folder (aka the |
| * <code>sling.home</code>. |
| * @return The absolute <code>File</code> indicating the launchpad folder. |
| */ |
| private File getLaunchpadHome(final String slingHome) { |
| String launchpadHomeParam = properties.get(SharedConstants.SLING_LAUNCHPAD); |
| if (launchpadHomeParam == null || launchpadHomeParam.length() == 0) { |
| properties.put(SharedConstants.SLING_LAUNCHPAD, slingHome); |
| return new File(slingHome); |
| } |
| |
| File launchpadHome = new File(launchpadHomeParam); |
| if (!launchpadHome.isAbsolute()) { |
| launchpadHome = new File(slingHome, launchpadHomeParam); |
| } |
| |
| properties.put(SharedConstants.SLING_LAUNCHPAD, |
| launchpadHome.getAbsolutePath()); |
| return launchpadHome; |
| } |
| |
| /** |
| * Converts the servlet context path to a path used for the sling.home |
| * property. The servlet context path is converted to a simple name by |
| * replacing all slash characters in the path with underscores (or a single |
| * underscore for the root context path being empty or null). Next the |
| * result is prefixed with either value of the |
| * <code>sling.home.prefix</code> system property or the (default prefix) |
| * <code>sling/</code>. |
| * |
| * @param contextPath |
| * @return |
| */ |
| private String toSlingHome(String contextPath) { |
| String prefix = System.getProperty(SLING_HOME_PREFIX, |
| SLING_HOME_PREFIX_DEFAULT); |
| |
| if (!prefix.endsWith("/")) { |
| prefix = prefix.concat("/"); |
| } |
| |
| if (contextPath == null || contextPath.length() == 0) { |
| return prefix + "_"; |
| } |
| |
| return prefix + contextPath.replace('/', '_'); |
| } |
| |
| private void startupFailure(String message, Throwable cause) { |
| |
| // ensure message |
| if (message == null) { |
| message = "Failed to start Apache Sling in " + slingHome; |
| } |
| |
| // unwrap to get the real cause |
| while (cause.getCause() != null) { |
| cause = cause.getCause(); |
| } |
| |
| // log it now and increase the failure counter |
| log(message, cause); |
| startFailureCounter++; |
| |
| // ensure the startingSling fields is not set |
| synchronized (this) { |
| startingSling = null; |
| } |
| } |
| |
| // ---------- Property file variable substition support -------------------- |
| |
| private Map<String, String> collectInitParameters() { |
| HashMap<String, String> props = new HashMap<String, String>(); |
| for (Enumeration<String> keys = getServletContext().getInitParameterNames(); keys.hasMoreElements();) { |
| String key = keys.nextElement(); |
| props.put(key, getServletContext().getInitParameter(key)); |
| } |
| for (Enumeration<String> keys = getServletConfig().getInitParameterNames(); keys.hasMoreElements();) { |
| String key = keys.nextElement(); |
| props.put(key, getServletConfig().getInitParameter(key)); |
| } |
| return props; |
| } |
| |
| } |