org.apache.sling.servlets.resolver.internal.SlingServletResolver.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.servlets.resolver.internal.SlingServletResolver.java

Source

/*
 * 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.servlets.resolver.internal;

import static org.apache.sling.api.SlingConstants.ERROR_MESSAGE;
import static org.apache.sling.api.SlingConstants.ERROR_SERVLET_NAME;
import static org.apache.sling.api.SlingConstants.ERROR_STATUS;
import static org.apache.sling.api.SlingConstants.SLING_CURRENT_SERVLET_NAME;
import static org.apache.sling.servlets.resolver.internal.ServletResolverConstants.SLING_SERLVET_NAME;
import static org.osgi.framework.Constants.SERVICE_ID;
import static org.osgi.framework.Constants.SERVICE_PID;
import static org.osgi.service.component.ComponentConstants.COMPONENT_NAME;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.management.NotCompliantMBeanException;
import javax.management.StandardMBean;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyUnbounded;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.request.RequestProgressTracker;
import org.apache.sling.api.request.RequestUtil;
import org.apache.sling.api.request.SlingRequestEvent;
import org.apache.sling.api.request.SlingRequestListener;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.SyntheticResource;
import org.apache.sling.api.scripting.SlingScript;
import org.apache.sling.api.scripting.SlingScriptResolver;
import org.apache.sling.api.servlets.OptingServlet;
import org.apache.sling.api.servlets.ServletResolver;
import org.apache.sling.commons.osgi.OsgiUtil;
import org.apache.sling.engine.ResponseUtil;
import org.apache.sling.engine.servlets.ErrorHandler;
import org.apache.sling.servlets.resolver.internal.defaults.DefaultErrorHandlerServlet;
import org.apache.sling.servlets.resolver.internal.defaults.DefaultServlet;
import org.apache.sling.servlets.resolver.internal.helper.AbstractResourceCollector;
import org.apache.sling.servlets.resolver.internal.helper.NamedScriptResourceCollector;
import org.apache.sling.servlets.resolver.internal.helper.ResourceCollector;
import org.apache.sling.servlets.resolver.internal.helper.SlingServletConfig;
import org.apache.sling.servlets.resolver.internal.resource.ServletResourceProvider;
import org.apache.sling.servlets.resolver.internal.resource.ServletResourceProviderFactory;
import org.apache.sling.servlets.resolver.jmx.SlingServletResolverCacheMBean;
import org.apache.sling.spi.resource.provider.ResourceProvider;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The <code>SlingServletResolver</code> has two functions: It resolves scripts
 * by implementing the {@link SlingScriptResolver} interface and it resolves a
 * servlet for a request by implementing the {@link ServletResolver} interface.
 *
 * The resolver uses an own session to find the scripts.
 *
 */
@Component(name = "org.apache.sling.servlets.resolver.SlingServletResolver", metatype = true, label = "%servletresolver.name", description = "%servletresolver.description")
@Service(value = { ServletResolver.class, SlingScriptResolver.class, ErrorHandler.class,
        SlingRequestListener.class })
@Properties({ @Property(name = "service.description", value = "Sling Servlet Resolver and Error Handler"),
        @Property(name = "event.topics", propertyPrivate = true, value = {
                "org/apache/sling/api/resource/Resource/*", "org/apache/sling/api/resource/ResourceProvider/*",
                "javax/script/ScriptEngineFactory/*", "org/apache/sling/api/adapter/AdapterFactory/*",
                "org/apache/sling/scripting/core/BindingsValuesProvider/*" }) })
@Reference(name = "Servlet", referenceInterface = javax.servlet.Servlet.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public class SlingServletResolver
        implements ServletResolver, SlingScriptResolver, SlingRequestListener, ErrorHandler, EventHandler {

    /**
     * The default servlet root is the first search path (which is usally /apps)
     */
    public static final String DEFAULT_SERVLET_ROOT = "0";

    /** The default cache size for the script resolution. */
    public static final int DEFAULT_CACHE_SIZE = 200;

    /** Servlet resolver logger */
    public static final Logger LOGGER = LoggerFactory.getLogger(SlingServletResolver.class);

    @Property(value = DEFAULT_SERVLET_ROOT)
    public static final String PROP_SERVLET_ROOT = "servletresolver.servletRoot";

    @Property
    public static final String PROP_SCRIPT_USER = "servletresolver.scriptUser";

    @Property(intValue = DEFAULT_CACHE_SIZE)
    public static final String PROP_CACHE_SIZE = "servletresolver.cacheSize";

    private static final String REF_SERVLET = "Servlet";

    @Property(value = "/", unbounded = PropertyUnbounded.ARRAY)
    public static final String PROP_PATHS = "servletresolver.paths";

    private static final String[] DEFAULT_PATHS = new String[] { "/" };

    @Property(value = "html", unbounded = PropertyUnbounded.ARRAY)
    public static final String PROP_DEFAULT_EXTENSIONS = "servletresolver.defaultExtensions";

    private static final String[] DEFAULT_DEFAULT_EXTENSIONS = new String[] { "html" };

    @Reference
    private ServletContext servletContext;

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    private ResourceResolver sharedScriptResolver;

    private final Map<ServiceReference, ServletReg> servletsByReference = new HashMap<ServiceReference, ServletReg>();

    private final List<ServiceReference> pendingServlets = new ArrayList<ServiceReference>();

    /** The component context. */
    private ComponentContext context;

    private ServletResourceProviderFactory servletResourceProviderFactory;

    // the default servlet if no other servlet applies for a request. This
    // field is set on demand by getDefaultServlet()
    private Servlet defaultServlet;

    // the default error handler servlet if no other error servlet applies for
    // a request. This field is set on demand by getDefaultErrorServlet()
    private Servlet fallbackErrorServlet;

    /** The script resolution cache. */
    private Map<AbstractResourceCollector, Servlet> cache;

    /** The cache size. */
    private int cacheSize;

    /** Flag to log warning if cache size exceed only once. */
    private volatile boolean logCacheSizeWarning;

    /** Registration as event handler. */
    private ServiceRegistration eventHandlerReg;

    /**
     * The allowed execution paths.
     */
    private String[] executionPaths;

    /**
     * The search paths
     */
    private String[] searchPaths;

    /**
     * The default extensions
     */
    private String[] defaultExtensions;

    private ServletResolverWebConsolePlugin plugin;

    // ---------- ServletResolver interface -----------------------------------

    /**
     * @see org.apache.sling.api.servlets.ServletResolver#resolveServlet(org.apache.sling.api.SlingHttpServletRequest)
     */
    @Override
    public Servlet resolveServlet(final SlingHttpServletRequest request) {
        final Resource resource = request.getResource();

        // start tracking servlet resolution
        final RequestProgressTracker tracker = request.getRequestProgressTracker();
        final String timerName = "resolveServlet(" + resource.getPath() + ")";
        tracker.startTimer(timerName);

        final String type = resource.getResourceType();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("resolveServlet called for resource {}", resource);
        }

        final ResourceResolver scriptResolver = this.getScriptResourceResolver();
        Servlet servlet = null;

        if (type != null && type.length() > 0) {
            servlet = resolveServletInternal(request, null, type, scriptResolver);
        }

        // last resort, use the core bundle default servlet
        if (servlet == null) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("No specific servlet found, trying default");
            }
            servlet = getDefaultServlet();
        }

        // track servlet resolution termination
        if (servlet == null) {
            tracker.logTimer(timerName, "Servlet resolution failed. See log for details");
        } else {
            tracker.logTimer(timerName, "Using servlet {0}", RequestUtil.getServletName(servlet));
        }

        // log the servlet found
        if (LOGGER.isDebugEnabled()) {
            if (servlet != null) {
                LOGGER.debug("Servlet {} found for resource={}", RequestUtil.getServletName(servlet), resource);
            } else {
                LOGGER.debug("No servlet found for resource={}", resource);
            }
        }

        return servlet;
    }

    /**
     * @see org.apache.sling.api.servlets.ServletResolver#resolveServlet(org.apache.sling.api.resource.Resource, java.lang.String)
     */
    @Override
    public Servlet resolveServlet(final Resource resource, final String scriptName) {
        if (resource == null) {
            throw new IllegalArgumentException("Resource must not be null");
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("resolveServlet called for resource {} with script name {}", resource, scriptName);
        }

        final ResourceResolver scriptResolver = this.getScriptResourceResolver();
        final Servlet servlet = resolveServletInternal(null, resource, scriptName, scriptResolver);

        // log the servlet found
        if (LOGGER.isDebugEnabled()) {
            if (servlet != null) {
                LOGGER.debug("Servlet {} found for resource {} and script name {}",
                        new Object[] { RequestUtil.getServletName(servlet), resource, scriptName });
            } else {
                LOGGER.debug("No servlet found for resource {} and script name {}", resource, scriptName);
            }
        }

        return servlet;
    }

    /**
     * @see org.apache.sling.api.servlets.ServletResolver#resolveServlet(org.apache.sling.api.resource.ResourceResolver, java.lang.String)
     */
    @Override
    public Servlet resolveServlet(final ResourceResolver resolver, final String scriptName) {
        if (resolver == null) {
            throw new IllegalArgumentException("Resource resolver must not be null");
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("resolveServlet called for for script name {}", scriptName);
        }

        final ResourceResolver scriptResolver = this.getScriptResourceResolver();
        final Servlet servlet = resolveServletInternal(null, (Resource) null, scriptName, scriptResolver);

        // log the servlet found
        if (LOGGER.isDebugEnabled()) {
            if (servlet != null) {
                LOGGER.debug("Servlet {} found for script name {}", RequestUtil.getServletName(servlet),
                        scriptName);
            } else {
                LOGGER.debug("No servlet found for script name {}", scriptName);
            }
        }

        return servlet;
    }

    /**
     * Get the servlet for the resource.
     */
    private Servlet getServlet(final Resource scriptResource) {
        // no resource -> no servlet
        if (scriptResource == null) {
            return null;
        }
        // if resource is fetched using shared resource resolver
        // or resource is a servlet resource, just adapt to servlet
        if (scriptResource.getResourceResolver() == this.sharedScriptResolver
                || "sling/bundle/resource".equals(scriptResource.getResourceSuperType())) {
            return scriptResource.adaptTo(Servlet.class);
        }
        // return a resource wrapper to make sure the implementation
        // switches from the per thread resource resolver to the shared once
        // the per thread resource resolver is closed
        return new ScriptResource(scriptResource, perThreadScriptResolver, this.sharedScriptResolver)
                .adaptTo(Servlet.class);
    }

    // ---------- ScriptResolver interface ------------------------------------

    /**
     * @see org.apache.sling.api.scripting.SlingScriptResolver#findScript(org.apache.sling.api.resource.ResourceResolver, java.lang.String)
     */
    @Override
    public SlingScript findScript(final ResourceResolver resourceResolver, final String name)
            throws SlingException {

        // is the path absolute
        SlingScript script = null;
        if (name.startsWith("/")) {

            final String path = ResourceUtil.normalize(name);
            if (this.isPathAllowed(path)) {
                final Resource resource = resourceResolver.getResource(path);
                if (resource != null) {
                    script = resource.adaptTo(SlingScript.class);
                }
            }
        } else {

            // relative script resolution against search path
            final String[] path = resourceResolver.getSearchPath();
            for (int i = 0; script == null && i < path.length; i++) {
                final String scriptPath = ResourceUtil.normalize(path[i] + name);
                if (this.isPathAllowed(scriptPath)) {
                    final Resource resource = resourceResolver.getResource(scriptPath);
                    if (resource != null) {
                        script = resource.adaptTo(SlingScript.class);
                    }
                }
            }

        }

        // some logging
        if (script != null) {
            LOGGER.debug("findScript: Using script {} for {}", script.getScriptResource().getPath(), name);
        } else {
            LOGGER.info("findScript: No script {} found in path", name);
        }

        // and finally return the script (null or not)
        return script;
    }

    // ---------- ErrorHandler interface --------------------------------------

    /**
     * @see org.apache.sling.engine.servlets.ErrorHandler#handleError(int,
     *      String, SlingHttpServletRequest, SlingHttpServletResponse)
     */
    @Override
    public void handleError(final int status, final String message, final SlingHttpServletRequest request,
            final SlingHttpServletResponse response) throws IOException {

        // do not handle, if already handling ....
        if (request.getAttribute(SlingConstants.ERROR_REQUEST_URI) != null) {
            LOGGER.error("handleError: Recursive invocation. Not further handling status " + status + "(" + message
                    + ")");
            return;
        }

        // start tracker
        RequestProgressTracker tracker = request.getRequestProgressTracker();
        String timerName = "handleError:status=" + status;
        tracker.startTimer(timerName);

        final ResourceResolver scriptResolver = this.getScriptResourceResolver();
        try {
            // find the error handler component
            Resource resource = getErrorResource(request);

            // find a servlet for the status as the method name
            ResourceCollector locationUtil = new ResourceCollector(String.valueOf(status),
                    ServletResolverConstants.ERROR_HANDLER_PATH, resource, this.executionPaths);
            Servlet servlet = getServletInternal(locationUtil, request, scriptResolver);

            // fall back to default servlet if none
            if (servlet == null) {
                servlet = getDefaultErrorServlet(request, resource, scriptResolver);
            }

            // set the message properties
            request.setAttribute(ERROR_STATUS, new Integer(status));
            request.setAttribute(ERROR_MESSAGE, message);

            // the servlet name for a sendError handling is still stored
            // as the request attribute
            Object servletName = request.getAttribute(SLING_CURRENT_SERVLET_NAME);
            if (servletName instanceof String) {
                request.setAttribute(ERROR_SERVLET_NAME, servletName);
            }

            // log a track entry after resolution before calling the handler
            tracker.logTimer(timerName, "Using handler {0}", RequestUtil.getServletName(servlet));

            handleError(servlet, request, response);

        } finally {
            tracker.logTimer(timerName, "Error handler finished");
        }
    }

    /**
     * @see org.apache.sling.engine.servlets.ErrorHandler#handleError(java.lang.Throwable, org.apache.sling.api.SlingHttpServletRequest, org.apache.sling.api.SlingHttpServletResponse)
     */
    @Override
    public void handleError(final Throwable throwable, final SlingHttpServletRequest request,
            final SlingHttpServletResponse response) throws IOException {
        // do not handle, if already handling ....
        if (request.getAttribute(SlingConstants.ERROR_REQUEST_URI) != null) {
            LOGGER.error("handleError: Recursive invocation. Not further handling Throwable:", throwable);
            return;
        }

        // start tracker
        RequestProgressTracker tracker = request.getRequestProgressTracker();
        String timerName = "handleError:throwable=" + throwable.getClass().getName();
        tracker.startTimer(timerName);

        final ResourceResolver scriptResolver = this.getScriptResourceResolver();
        try {
            // find the error handler component
            Servlet servlet = null;
            Resource resource = getErrorResource(request);

            Class<?> tClass = throwable.getClass();
            while (servlet == null && tClass != Object.class) {
                // find a servlet for the simple class name as the method name
                ResourceCollector locationUtil = new ResourceCollector(tClass.getSimpleName(),
                        ServletResolverConstants.ERROR_HANDLER_PATH, resource, this.executionPaths);
                servlet = getServletInternal(locationUtil, request, scriptResolver);

                // go to the base class
                tClass = tClass.getSuperclass();
            }

            if (servlet == null) {
                servlet = getDefaultErrorServlet(request, resource, scriptResolver);
            }

            // set the message properties
            request.setAttribute(SlingConstants.ERROR_EXCEPTION, throwable);
            request.setAttribute(SlingConstants.ERROR_EXCEPTION_TYPE, throwable.getClass());
            request.setAttribute(SlingConstants.ERROR_MESSAGE, throwable.getMessage());

            // log a track entry after resolution before calling the handler
            tracker.logTimer(timerName, "Using handler {0}", RequestUtil.getServletName(servlet));

            handleError(servlet, request, response);
        } finally {
            tracker.logTimer(timerName, "Error handler finished");
        }
    }

    // ---------- internal helper ---------------------------------------------

    private ResourceResolver getScriptResourceResolver() {
        ResourceResolver scriptResolver = this.perThreadScriptResolver.get();
        if (scriptResolver == null) {
            // no per thread, let's use the shared one
            synchronized (this.sharedScriptResolver) {
                this.sharedScriptResolver.refresh();
            }
            scriptResolver = this.sharedScriptResolver;
        }
        return scriptResolver;
    }

    private final ThreadLocal<ResourceResolver> perThreadScriptResolver = new ThreadLocal<ResourceResolver>();

    private ServiceRegistration mbeanRegistration;

    /**
     * @see org.apache.sling.api.request.SlingRequestListener#onEvent(org.apache.sling.api.request.SlingRequestEvent)
     */
    @Override
    public void onEvent(final SlingRequestEvent event) {
        if (event.getType() == SlingRequestEvent.EventType.EVENT_INIT) {
            try {
                this.perThreadScriptResolver.set(this.sharedScriptResolver.clone(null));
            } catch (final LoginException e) {
                LOGGER.error("Unable to create new script resolver clone", e);
            }
        } else if (event.getType() == SlingRequestEvent.EventType.EVENT_DESTROY) {
            final ResourceResolver resolver = this.perThreadScriptResolver.get();
            if (resolver != null) {
                this.perThreadScriptResolver.remove();
                resolver.close();
            }
        }
    }

    /**
     * Returns the resource of the given request to be used as the basis for
     * error handling. If the resource has not yet been set in the request
     * because the error occurred before the resource could be set (e.g. during
     * resource resolution) a synthetic resource is returned whose type is
     * {@link ServletResolverConstants#ERROR_HANDLER_PATH}.
     *
     * @param request The request whose resource is to be returned.
     */
    private Resource getErrorResource(final SlingHttpServletRequest request) {
        Resource res = request.getResource();
        if (res == null) {
            res = new SyntheticResource(request.getResourceResolver(), request.getPathInfo(),
                    ServletResolverConstants.ERROR_HANDLER_PATH);
        }
        return res;
    }

    /**
    * Resolve an appropriate servlet for a given request and resource type
    * using the provided ResourceResolver
    */
    private Servlet resolveServletInternal(final SlingHttpServletRequest request, final Resource resource,
            final String scriptName, final ResourceResolver resolver) {
        Servlet servlet = null;

        // first check whether the type of a resource is the absolute
        // path of a servlet (or script)
        if (scriptName.charAt(0) == '/') {
            final String scriptPath = ResourceUtil.normalize(scriptName);
            if (this.isPathAllowed(scriptPath)) {
                final Resource res = resolver.getResource(scriptPath);
                servlet = this.getServlet(res);
                if (servlet != null && LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Servlet {} found using absolute resource type {}",
                            RequestUtil.getServletName(servlet), scriptName);
                }
            } else {
                if (request != null) {
                    request.getRequestProgressTracker().log(
                            "Will not look for a servlet at {0} as it is not in the list of allowed paths",
                            scriptName);
                }
            }
        }
        if (servlet == null) {
            // the resource type is not absolute, so lets go for the deep search
            final AbstractResourceCollector locationUtil;
            if (request != null) {
                locationUtil = ResourceCollector.create(request, this.executionPaths, this.defaultExtensions);
            } else {
                locationUtil = NamedScriptResourceCollector.create(scriptName, resource, this.executionPaths);
            }
            servlet = getServletInternal(locationUtil, request, resolver);

            if (servlet != null && LOGGER.isDebugEnabled()) {
                LOGGER.debug("getServletInternal returns servlet {}", RequestUtil.getServletName(servlet));
            }
        }
        return servlet;
    }

    /**
     * Returns a servlet suitable for handling a request. The
     * <code>locationUtil</code> is used find any servlets or scripts usable for
     * the request. Each servlet returned is in turn asked whether it is
     * actually willing to handle the request in case the servlet is an
     * <code>OptingServlet</code>. The first servlet willing to handle the
     * request is used.
     *
     * @param locationUtil The helper used to find appropriate servlets ordered
     *            by matching priority.
     * @param request The request used to give to any <code>OptingServlet</code>
     *            for them to decide on whether they are willing to handle the
     *            request
     * @param resolver The <code>ResourceResolver</code> used for resolving the servlets.
     * @return a servlet for handling the request or <code>null</code> if no
     *         such servlet willing to handle the request could be found.
     */
    private Servlet getServletInternal(final AbstractResourceCollector locationUtil,
            final SlingHttpServletRequest request, final ResourceResolver resolver) {
        final Servlet scriptServlet = (this.cache != null ? this.cache.get(locationUtil) : null);
        if (scriptServlet != null) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Using cached servlet {}", RequestUtil.getServletName(scriptServlet));
            }
            return scriptServlet;
        }

        final Collection<Resource> candidates = locationUtil.getServlets(resolver);

        if (LOGGER.isDebugEnabled()) {
            if (candidates.isEmpty()) {
                LOGGER.debug("No servlet candidates found");
            } else {
                LOGGER.debug("Ordered list of servlet candidates follows");
                for (Resource candidateResource : candidates) {
                    LOGGER.debug("Servlet candidate: {}", candidateResource.getPath());
                }
            }
        }

        boolean hasOptingServlet = false;
        for (final Resource candidateResource : candidates) {
            LOGGER.debug("Checking if candidate resource {} adapts to servlet and accepts request",
                    candidateResource.getPath());
            Servlet candidate = this.getServlet(candidateResource);
            if (candidate != null) {
                final boolean isOptingServlet = candidate instanceof OptingServlet;
                boolean servletAcceptsRequest = !isOptingServlet
                        || (request != null && ((OptingServlet) candidate).accepts(request));
                if (servletAcceptsRequest) {
                    if (!hasOptingServlet && !isOptingServlet && this.cache != null) {
                        if (this.cache.size() < this.cacheSize) {
                            this.cache.put(locationUtil, candidate);
                        } else if (this.logCacheSizeWarning) {
                            this.logCacheSizeWarning = false;
                            LOGGER.warn(
                                    "Script cache has reached its limit of {}. You might want to increase the cache size for the servlet resolver.",
                                    this.cacheSize);
                        }
                    }
                    LOGGER.debug("Using servlet provided by candidate resource {}", candidateResource.getPath());
                    return candidate;
                }
                if (isOptingServlet) {
                    hasOptingServlet = true;
                }
                LOGGER.debug("Candidate {} does not accept request, ignored", candidateResource.getPath());
            } else {
                LOGGER.debug("Candidate {} does not adapt to a servlet, ignored", candidateResource.getPath());
            }
        }

        // exhausted all candidates, we don't have a servlet
        return null;
    }

    /**
     * Returns the internal default servlet which is called in case no other
     * servlet applies for handling a request. This servlet should really only
     * be used if the default servlets have not been registered (yet).
     */
    private Servlet getDefaultServlet() {
        if (defaultServlet == null) {
            try {
                Servlet servlet = new DefaultServlet();
                servlet.init(new SlingServletConfig(servletContext, null, "Apache Sling Core Default Servlet"));
                defaultServlet = servlet;
            } catch (final ServletException se) {
                LOGGER.error("Failed to initialize default servlet", se);
            }
        }

        return defaultServlet;
    }

    /**
     * Returns the default error handler servlet, which is called in case there
     * is no other - better matching - servlet registered to handle an error or
     * exception.
     * <p>
     * The default error handler servlet is registered for the resource type
     * "sling/servlet/errorhandler" and method "default". This may be
     * overwritten by applications globally or according to the resource type
     * hierarchy of the resource.
     * <p>
     * If no default error handler servlet can be found an adhoc error handler
     * is used as a final fallback.
     */
    private Servlet getDefaultErrorServlet(final SlingHttpServletRequest request, final Resource resource,
            final ResourceResolver resolver) {

        // find a default error handler according to the resource type
        // tree of the given resource
        final ResourceCollector locationUtil = new ResourceCollector(
                ServletResolverConstants.DEFAULT_ERROR_HANDLER_NAME, ServletResolverConstants.ERROR_HANDLER_PATH,
                resource, this.executionPaths);
        final Servlet servlet = getServletInternal(locationUtil, request, resolver);
        if (servlet != null) {
            return servlet;
        }

        // if no registered default error handler could be found use
        // the DefaultErrorHandlerServlet as an ad-hoc fallback
        if (fallbackErrorServlet == null) {
            // fall back to an adhoc instance of the DefaultErrorHandlerServlet
            // if the actual service is not registered (yet ?)
            try {
                final Servlet defaultServlet = new DefaultErrorHandlerServlet();
                defaultServlet.init(new SlingServletConfig(servletContext, null,
                        "Sling (Ad Hoc) Default Error Handler Servlet"));
                fallbackErrorServlet = defaultServlet;
            } catch (ServletException se) {
                LOGGER.error("Failed to initialize error servlet", se);
            }
        }
        return fallbackErrorServlet;
    }

    private void handleError(final Servlet errorHandler, final HttpServletRequest request,
            final HttpServletResponse response) throws IOException {

        request.setAttribute(SlingConstants.ERROR_REQUEST_URI, request.getRequestURI());

        // if there is no explicitly known error causing servlet, use
        // the name of the error handler servlet
        if (request.getAttribute(SlingConstants.ERROR_SERVLET_NAME) == null) {
            request.setAttribute(SlingConstants.ERROR_SERVLET_NAME,
                    errorHandler.getServletConfig().getServletName());
        }

        // Let the error handler servlet process the request and
        // forward all exceptions if it fails.
        // Before SLING-4143 we only forwarded IOExceptions.
        try {
            errorHandler.service(request, response);
            // commit the response
            response.flushBuffer();
            // close the response (SLING-2724)
            response.getWriter().close();
        } catch (final Throwable t) {
            LOGGER.error("Calling the error handler resulted in an error", t);
            LOGGER.error("Original error " + request.getAttribute(SlingConstants.ERROR_EXCEPTION_TYPE),
                    (Throwable) request.getAttribute(SlingConstants.ERROR_EXCEPTION));
            final IOException x = new IOException("Error handler failed: " + t.getClass().getName());
            x.initCause(t);
            throw x;
        }
    }

    private Map<String, Object> createAuthenticationInfo(final Dictionary<String, Object> props) {
        final Map<String, Object> authInfo = new HashMap<String, Object>();
        // if a script user is configured we use this user to read the scripts
        final String scriptUser = OsgiUtil.toString(props.get(PROP_SCRIPT_USER), null);
        if (scriptUser != null && scriptUser.length() > 0) {
            authInfo.put(ResourceResolverFactory.USER_IMPERSONATION, scriptUser);
        }
        return authInfo;
    }

    // ---------- SCR Integration ----------------------------------------------

    /**
     * Activate this component.
     */
    @SuppressWarnings("unchecked")
    protected void activate(final ComponentContext context) throws LoginException {
        // from configuration if available
        final Dictionary<?, ?> properties = context.getProperties();
        Object servletRoot = properties.get(PROP_SERVLET_ROOT);
        if (servletRoot == null) {
            servletRoot = DEFAULT_SERVLET_ROOT;
        }

        final Collection<ServiceReference> refs;
        synchronized (this.pendingServlets) {

            refs = new ArrayList<ServiceReference>(pendingServlets);
            pendingServlets.clear();

            this.sharedScriptResolver = resourceResolverFactory
                    .getAdministrativeResourceResolver(this.createAuthenticationInfo(context.getProperties()));
            this.searchPaths = this.sharedScriptResolver.getSearchPath();
            servletResourceProviderFactory = new ServletResourceProviderFactory(servletRoot, this.searchPaths);

            // register servlets immediately from now on
            this.context = context;
        }
        createAllServlets(refs);

        // execution paths
        this.executionPaths = OsgiUtil.toStringArray(properties.get(PROP_PATHS), DEFAULT_PATHS);
        if (this.executionPaths != null) {
            // if we find a string combination that basically allows all paths,
            // we simply set the array to null
            if (this.executionPaths.length == 0) {
                this.executionPaths = null;
            } else {
                boolean hasRoot = false;
                for (int i = 0; i < this.executionPaths.length; i++) {
                    final String path = this.executionPaths[i];
                    if (path == null || path.length() == 0 || path.equals("/")) {
                        hasRoot = true;
                        break;
                    }
                }
                if (hasRoot) {
                    this.executionPaths = null;
                }
            }
        }
        this.defaultExtensions = OsgiUtil.toStringArray(properties.get(PROP_DEFAULT_EXTENSIONS),
                DEFAULT_DEFAULT_EXTENSIONS);

        // create cache - if a cache size is configured
        this.cacheSize = OsgiUtil.toInteger(properties.get(PROP_CACHE_SIZE), DEFAULT_CACHE_SIZE);
        if (this.cacheSize > 5) {
            this.cache = new ConcurrentHashMap<AbstractResourceCollector, Servlet>(cacheSize);
            this.logCacheSizeWarning = true;
        } else {
            this.cacheSize = 0;
        }

        // setup default servlet
        this.getDefaultServlet();

        // and finally register as event listener
        this.eventHandlerReg = context.getBundleContext().registerService(EventHandler.class.getName(), this,
                properties);

        this.plugin = new ServletResolverWebConsolePlugin(context.getBundleContext());

        if (this.cacheSize > 0) {
            try {
                Dictionary<String, String> mbeanProps = new Hashtable<String, String>();
                mbeanProps.put("jmx.objectname",
                        "org.apache.sling:type=servletResolver,service=SlingServletResolverCache");

                ServletResolverCacheMBeanImpl mbean = new ServletResolverCacheMBeanImpl();
                mbeanRegistration = context.getBundleContext()
                        .registerService(SlingServletResolverCacheMBean.class.getName(), mbean, mbeanProps);
            } catch (Throwable t) {
                LOGGER.debug("Unable to register mbean");
            }
        }
    }

    /**
     * Deactivate this component.
     */
    protected void deactivate(final ComponentContext context) {
        // stop registering of servlets immediately
        this.context = null;

        if (this.plugin != null) {
            this.plugin.dispose();
        }

        // unregister event handler
        if (this.eventHandlerReg != null) {
            this.eventHandlerReg.unregister();
            this.eventHandlerReg = null;
        }

        // Copy the list of servlets first, to minimize the need for
        // synchronization
        final Collection<ServiceReference> refs;
        synchronized (this.servletsByReference) {
            refs = new ArrayList<ServiceReference>(servletsByReference.keySet());
        }
        // destroy all servlets
        destroyAllServlets(refs);

        // sanity check: clear array (it should be empty now anyway)
        synchronized (this.servletsByReference) {
            this.servletsByReference.clear();
        }

        // destroy the fallback error handler servlet
        if (fallbackErrorServlet != null) {
            try {
                fallbackErrorServlet.destroy();
            } catch (Throwable t) {
                // ignore
            } finally {
                fallbackErrorServlet = null;
            }
        }

        if (this.sharedScriptResolver != null) {
            this.sharedScriptResolver.close();
            this.sharedScriptResolver = null;
        }

        this.cache = null;
        this.servletResourceProviderFactory = null;

        if (this.mbeanRegistration != null) {
            this.mbeanRegistration.unregister();
            this.mbeanRegistration = null;
        }
    }

    protected void bindServlet(final ServiceReference reference) {
        boolean directCreate = true;
        if (context == null) {
            synchronized (pendingServlets) {
                if (context == null) {
                    pendingServlets.add(reference);
                    directCreate = false;
                }
            }
        }
        if (directCreate) {
            createServlet(reference);
        }
    }

    protected void unbindServlet(final ServiceReference reference) {
        synchronized (pendingServlets) {
            pendingServlets.remove(reference);
        }
        destroyServlet(reference);
    }

    // ---------- Servlet Management -------------------------------------------

    private void createAllServlets(final Collection<ServiceReference> pendingServlets) {
        for (final ServiceReference serviceReference : pendingServlets) {
            createServlet(serviceReference);
        }
    }

    private boolean createServlet(final ServiceReference reference) {

        // check for a name, this is required
        final String name = getName(reference);
        if (name == null) {
            LOGGER.error("bindServlet: Cannot register servlet {} without a servlet name", reference);
            return false;
        }

        // check for Sling properties in the service registration
        ServletResourceProvider provider = servletResourceProviderFactory.create(reference);
        if (provider == null) {
            // this is expected if the servlet is not destined for Sling
            return false;
        }

        // only now try to access the servlet service, this may still fail
        Servlet servlet = null;
        try {
            servlet = (Servlet) context.locateService(REF_SERVLET, reference);
        } catch (Throwable t) {
            LOGGER.warn("bindServlet: Failed getting the service for reference " + reference, t);
        }
        if (servlet == null) {
            LOGGER.error("bindServlet: Servlet service not available from reference {}", reference);
            return false;
        }

        // assign the servlet to the provider
        provider.setServlet(servlet);

        // initialize now
        try {
            servlet.init(new SlingServletConfig(servletContext, reference, name));
            LOGGER.debug("bindServlet: Servlet {} added", name);
        } catch (ServletException ce) {
            LOGGER.error("bindServlet: Component " + name + " failed to initialize", ce);
            return false;
        } catch (Throwable t) {
            LOGGER.error("bindServlet: Unexpected problem initializing component " + name, t);
            return false;
        }

        final List<ServiceRegistration> regs = new ArrayList<ServiceRegistration>();
        for (final String root : provider.getServletPaths()) {
            final ServiceRegistration reg = context.getBundleContext().registerService(
                    ResourceProvider.class.getName(), provider, createServiceProperties(reference, provider, root));
            regs.add(reg);
        }
        LOGGER.debug("Registered {}", provider.toString());
        synchronized (this.servletsByReference) {
            servletsByReference.put(reference, new ServletReg(servlet, regs));
        }
        return true;
    }

    private Dictionary<String, Object> createServiceProperties(final ServiceReference reference,
            final ServletResourceProvider provider, final String root) {

        final Dictionary<String, Object> params = new Hashtable<String, Object>();
        params.put(ResourceProvider.PROPERTY_ROOT, root);
        params.put(Constants.SERVICE_DESCRIPTION,
                "ServletResourceProvider for Servlets at " + Arrays.asList(provider.getServletPaths()));

        // inherit service ranking
        Object rank = reference.getProperty(Constants.SERVICE_RANKING);
        if (rank instanceof Integer) {
            params.put(Constants.SERVICE_RANKING, rank);
        }

        return params;
    }

    private void destroyAllServlets(final Collection<ServiceReference> refs) {
        for (ServiceReference serviceReference : refs) {
            destroyServlet(serviceReference);
        }
    }

    private void destroyServlet(final ServiceReference reference) {
        ServletReg registration;
        synchronized (this.servletsByReference) {
            registration = servletsByReference.remove(reference);
        }
        if (registration != null) {

            for (final ServiceRegistration reg : registration.registrations) {
                reg.unregister();
            }
            final String name = RequestUtil.getServletName(registration.servlet);
            LOGGER.debug("unbindServlet: Servlet {} removed", name);

            try {
                registration.servlet.destroy();
            } catch (Throwable t) {
                LOGGER.error("unbindServlet: Unexpected problem destroying servlet " + name, t);
            }
        }
    }

    /**
     * @see org.osgi.service.event.EventHandler#handleEvent(org.osgi.service.event.Event)
     */
    @Override
    public void handleEvent(final Event event) {
        if (this.cache != null) {
            boolean flushCache = false;

            // we may receive different events
            final String topic = event.getTopic();
            if (topic.startsWith("javax/script/ScriptEngineFactory/")) {
                // script engine factory added or removed: we always flush
                flushCache = true;
            } else if (topic.startsWith("org/apache/sling/api/adapter/AdapterFactory/")) {
                // adapter factory added or removed: we always flush
                // as adapting might be transitive
                flushCache = true;
            } else if (topic.startsWith("org/apache/sling/scripting/core/BindingsValuesProvider/")) {
                // bindings values provide factory added or removed: we always flush
                flushCache = true;
            } else {
                // this is a resource or resource provider event

                // if the path of the event is a sub path of a search path
                // we flush the whole cache
                final String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH);
                if (path != null) {
                    int index = 0;
                    while (!flushCache && index < searchPaths.length) {
                        if (path.startsWith(this.searchPaths[index])) {
                            flushCache = true;
                        }
                        index++;
                    }
                }
            }
            if (flushCache) {
                flushCache();
            }
        }
    }

    private void flushCache() {
        this.cache.clear();
        this.logCacheSizeWarning = true;
    }

    /** The list of property names checked by {@link #getName(ServiceReference)} */
    private static final String[] NAME_PROPERTIES = { SLING_SERLVET_NAME, COMPONENT_NAME, SERVICE_PID, SERVICE_ID };

    /**
     * Looks for a name value in the service reference properties. See the
     * class comment at the top for the list of properties checked by this
     * method.
     */
    private static String getName(final ServiceReference reference) {
        String servletName = null;
        for (int i = 0; i < NAME_PROPERTIES.length && (servletName == null || servletName.length() == 0); i++) {
            Object prop = reference.getProperty(NAME_PROPERTIES[i]);
            if (prop != null) {
                servletName = String.valueOf(prop);
            }
        }
        return servletName;
    }

    private boolean isPathAllowed(final String path) {
        return AbstractResourceCollector.isPathAllowed(path, this.executionPaths);
    }

    private static final class ServletReg {
        public final Servlet servlet;
        public final List<ServiceRegistration> registrations;

        public ServletReg(final Servlet s, final List<ServiceRegistration> srs) {
            this.servlet = s;
            this.registrations = srs;
        }
    }

    @SuppressWarnings("serial")
    class ServletResolverWebConsolePlugin extends HttpServlet {
        private static final String PARAMETER_URL = "url";
        private static final String PARAMETER_METHOD = "method";

        private ServiceRegistration service;

        public ServletResolverWebConsolePlugin(final BundleContext context) {
            Dictionary<String, Object> props = new Hashtable<String, Object>();
            props.put(Constants.SERVICE_DESCRIPTION, "Sling Servlet Resolver Web Console Plugin");
            props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
            props.put(Constants.SERVICE_PID, getClass().getName());
            props.put("felix.webconsole.label", "servletresolver");
            props.put("felix.webconsole.title", "Sling Servlet Resolver");
            props.put("felix.webconsole.css", "/servletresolver/res/ui/styles.css");
            props.put("felix.webconsole.category", "Sling");

            service = context.registerService(new String[] { "javax.servlet.Servlet" }, this, props);
        }

        public void dispose() {
            if (service != null) {
                service.unregister();
                service = null;
            }
        }

        @Override
        protected void service(final HttpServletRequest request, final HttpServletResponse response)
                throws ServletException, IOException {
            final String url = request.getParameter(PARAMETER_URL);
            final RequestPathInfo requestPathInfo = new DecomposedURL(url).getRequestPathInfo();
            String method = request.getParameter(PARAMETER_METHOD);
            if (StringUtils.isBlank(method)) {
                method = "GET";
            }

            final String CONSOLE_PATH_WARNING = "<em>"
                    + "Note that in a real Sling request, the path might vary depending on the existence of"
                    + " resources that partially match it."
                    + "<br/>This utility does not take this into account and uses the first dot to split"
                    + " between path and selectors/extension."
                    + "<br/>As a workaround, you can replace dots with underline characters, for example, when testing such an URL."
                    + "</em>";

            ResourceResolver resourceResolver = null;
            try {
                resourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null);

                final PrintWriter pw = response.getWriter();

                pw.print("<form method='get'>");
                pw.println("<table class='content' cellpadding='0' cellspacing='0' width='100%'>");

                titleHtml(pw, "Servlet Resolver Test",
                        "To check which servlet is responsible for rendering a response, enter a request path into "
                                + "the field and click 'Resolve' to resolve it.");

                tr(pw);
                tdLabel(pw, "URL");
                tdContent(pw);

                pw.print("<input type='text' name='");
                pw.print(PARAMETER_URL);
                pw.print("' value='");
                if (url != null) {
                    pw.print(ResponseUtil.escapeXml(url));
                }
                pw.println("' class='input' size='50'>");
                closeTd(pw);
                closeTr(pw);
                closeTr(pw);

                tr(pw);
                tdLabel(pw, "Method");
                tdContent(pw);
                pw.print("<select name='");
                pw.print(PARAMETER_METHOD);
                pw.println("'>");
                pw.println("<option value='GET'>GET</option>");
                pw.println("<option value='POST'>POST</option>");
                pw.println("</select>");
                pw.println("&nbsp;&nbsp;<input type='submit' value='Resolve' class='submit'>");

                closeTd(pw);
                closeTr(pw);

                if (StringUtils.isNotBlank(url)) {
                    tr(pw);
                    tdLabel(pw, "Decomposed URL");
                    tdContent(pw);
                    pw.println("<dl>");
                    pw.println("<dt>Path</dt>");
                    pw.print("<dd>");
                    pw.print(ResponseUtil.escapeXml(requestPathInfo.getResourcePath()));
                    pw.print("<br/>");
                    pw.print(CONSOLE_PATH_WARNING);
                    pw.println("</dd>");
                    pw.println("<dt>Selectors</dt>");
                    pw.print("<dd>");
                    if (requestPathInfo.getSelectors().length == 0) {
                        pw.print("&lt;none&gt;");
                    } else {
                        pw.print("[");
                        pw.print(ResponseUtil.escapeXml(StringUtils.join(requestPathInfo.getSelectors(), ", ")));
                        pw.print("]");
                    }
                    pw.println("</dd>");
                    pw.println("<dt>Extension</dt>");
                    pw.print("<dd>");
                    pw.print(ResponseUtil.escapeXml(requestPathInfo.getExtension()));
                    pw.println("</dd>");
                    pw.println("</dl>");
                    pw.println("</dd>");
                    pw.println("<dt>Suffix</dt>");
                    pw.print("<dd>");
                    pw.print(ResponseUtil.escapeXml(requestPathInfo.getSuffix()));
                    pw.println("</dd>");
                    pw.println("</dl>");
                    closeTd(pw);
                    closeTr(pw);
                }

                if (StringUtils.isNotBlank(requestPathInfo.getResourcePath())) {
                    final Collection<Resource> servlets;
                    Resource resource = resourceResolver.resolve(requestPathInfo.getResourcePath());
                    if (resource.adaptTo(Servlet.class) != null) {
                        servlets = Collections.singleton(resource);
                    } else {
                        final ResourceCollector locationUtil = ResourceCollector.create(resource,
                                requestPathInfo.getExtension(), executionPaths, defaultExtensions, method,
                                requestPathInfo.getSelectors());
                        servlets = locationUtil.getServlets(resourceResolver);
                    }
                    tr(pw);
                    tdLabel(pw, "Candidates");
                    tdContent(pw);

                    if (servlets == null || servlets.isEmpty()) {
                        pw.println("Could not find a suitable servlet for this request!");
                    } else {
                        pw.print("Candidate servlets and scripts in order of preference for method ");
                        pw.print(ResponseUtil.escapeXml(method));
                        pw.println(":<br/>");
                        pw.println("<ol class='servlets'>");
                        outputServlets(pw, servlets.iterator());
                        pw.println("</ol>");
                    }
                    pw.println("</td>");
                    closeTr(pw);
                }

                pw.println("</table>");
                pw.print("</form>");
            } catch (LoginException e) {
                throw new ServletException(e);
            } finally {
                if (resourceResolver != null) {
                    resourceResolver.close();
                }
            }
        }

        private void tdContent(final PrintWriter pw) {
            pw.print("<td class='content' colspan='2'>");
        }

        private void closeTd(final PrintWriter pw) {
            pw.print("</td>");
        }

        @SuppressWarnings("unused")
        private URL getResource(final String path) {
            if (path.startsWith("/servletresolver/res/ui")) {
                return this.getClass().getResource(path.substring(16));
            } else {
                return null;
            }
        }

        private void closeTr(final PrintWriter pw) {
            pw.println("</tr>");
        }

        private void tdLabel(final PrintWriter pw, final String label) {
            pw.print("<td class='content'>");
            pw.print(ResponseUtil.escapeXml(label));
            pw.println("</td>");
        }

        private void tr(final PrintWriter pw) {
            pw.println("<tr class='content'>");
        }

        private void outputServlets(final PrintWriter pw, final Iterator<Resource> iterator) {
            while (iterator.hasNext()) {
                Resource candidateResource = iterator.next();
                Servlet candidate = candidateResource.adaptTo(Servlet.class);
                if (candidate != null) {
                    final boolean allowed = isPathAllowed(candidateResource.getPath());
                    pw.print("<li>");
                    if (!allowed) {
                        pw.print("<del>");
                    }

                    if (candidate instanceof SlingScript) {
                        pw.print(ResponseUtil.escapeXml(candidateResource.getPath()));
                    } else {
                        final boolean isOptingServlet = candidate instanceof OptingServlet;
                        pw.print(ResponseUtil.escapeXml((candidate.getClass().getName())));
                        if (isOptingServlet) {
                            pw.print(" (OptingServlet)");
                        }
                    }

                    if (!allowed) {
                        pw.print("</del>");
                    }
                    pw.println("</li>");
                }
            }
        }

        private void titleHtml(final PrintWriter pw, final String title, final String description) {
            tr(pw);
            pw.print("<th colspan='3' class='content container'>");
            pw.print(ResponseUtil.escapeXml(title));
            pw.println("</th>");
            closeTr(pw);

            if (description != null) {
                tr(pw);
                pw.print("<td colspan='3' class='content'>");
                pw.print(ResponseUtil.escapeXml(description));
                pw.println("</th>");
                closeTr(pw);
            }
        }

    }

    class ServletResolverCacheMBeanImpl extends StandardMBean implements SlingServletResolverCacheMBean {

        ServletResolverCacheMBeanImpl() throws NotCompliantMBeanException {
            super(SlingServletResolverCacheMBean.class);
        }

        @Override
        public int getCacheSize() {
            return cache != null ? cache.size() : 0;
        }

        @Override
        public void flushCache() {
            SlingServletResolver.this.flushCache();
        }

        @Override
        public int getMaximumCacheSize() {
            return cacheSize;
        }

    }
}