blob: 842d1446fffb9dbfd376b4bf2cb73deda1a74493 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.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.api.servlets.ServletResolverConstants.DEFAULT_ERROR_HANDLER_RESOURCE_TYPE;
import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_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 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.lang3.StringUtils;
import org.apache.sling.api.SlingConstants;
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.ResponseUtil;
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.servlets.OptingServlet;
import org.apache.sling.api.servlets.ServletResolver;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.engine.servlets.ErrorHandler;
import org.apache.sling.serviceusermapping.ServiceUserMapped;
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.resolution.ResolutionCache;
import org.apache.sling.servlets.resolver.internal.resource.ServletResourceProvider;
import org.apache.sling.servlets.resolver.internal.resource.ServletResourceProviderFactory;
import org.apache.sling.spi.resource.provider.ResourceProvider;
import org.osgi.framework.Bundle;
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.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>SlingServletResolver</code> resolves a
* servlet for a request by implementing the {@link ServletResolver} interface.
*
* The resolver uses an own session to find the scripts.
*
*/
@Component(name = SlingServletResolver.Config.PID,
service = { ServletResolver.class, ErrorHandler.class, SlingRequestListener.class },
property = {
Constants.SERVICE_DESCRIPTION + "=Apache Sling Servlet Resolver and Error Handler",
Constants.SERVICE_VENDOR + "=The Apache Software Foundation"
})
@Designate(ocd = SlingServletResolver.Config.class)
public class SlingServletResolver
implements ServletResolver,
SlingRequestListener,
ErrorHandler {
@ObjectClassDefinition(name = "Apache Sling Servlet/Script Resolver and Error Handler",
description= "The Sling Servlet and Script Resolver has "+
"multiple tasks: One it is used as the ServletResolver to select the Servlet "+
"or Script to call to handle the request. Second it acts as the "+
"SlingScriptResolver and finally it manages error handling by implementing "+
"the ErrorHandler interface using the same algorithm to select error handling "+
"servlets and scripts as is used to resolve request processing servlets and "+
"scripts.")
public @interface Config {
String PID = "org.apache.sling.servlets.resolver.SlingServletResolver";
/**
* The default servlet root is the first search path (which is usually /apps)
*/
@AttributeDefinition(name="Servlet Registration Root Path",
description = "The default root path assumed when "+
"registering a servlet whose servlet registration properties define a relative "+
"resource type/path. It can either be a string starting with \"/\" (specifying a path prefix to be used) "+
"or a number which specifies the resource resolver's search path entry index. The default value "+
"is 0 (usually stands for \"/apps\" in the search paths). The number can be -1 which always "+
"points to the last search path entry.")
String servletresolver_servletRoot() default "0";
/** The default cache size for the script resolution. */
@AttributeDefinition(name = "Cache Size",
description = "This property configures the size of the " +
"cache used for script resolution. A value lower than 5 disables the cache.")
int servletresolver_cacheSize() default 200;
@AttributeDefinition(name = "Execution Paths",
description = "The paths to search for executable scripts. If no path is configured " +
"this is treated like the default (/ = root) which allows to execute all scripts. By configuring some " +
"paths the execution of scripts can be limited. If a configured value ends with a slash, the whole sub tree " +
"is allowed. Without a slash an exact matching script is allowed.")
String[] servletresolver_paths() default "/";
@AttributeDefinition(name = "Default Extensions",
description = "The list of extensions for which the default behavior " +
"will be used. This means that the last path segment of the resource type can be used as the script name.")
String[] servletresolver_defaultExtensions() default "html";
}
/** Servlet resolver logger */
public static final Logger LOGGER = LoggerFactory.getLogger(SlingServletResolver.class);
private static final String REF_SERVLET = "Servlet";
@Reference(target="(name=org.apache.sling)")
private ServletContext servletContext;
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Reference(target="("+ServiceUserMapped.SUBSERVICENAME+"=scripts)")
private ServiceUserMapped scriptServiceUserMapped;
@Reference(target="("+ServiceUserMapped.SUBSERVICENAME+"=console)")
private ServiceUserMapped consoleServiceUserMapped;
@Reference
private ResolutionCache resolutionCache;
private List<String> scriptEnginesExtensions = new ArrayList<>();
private ResourceResolver sharedScriptResolver;
private final Map<ServiceReference<Servlet>, ServletReg> servletsByReference = new HashMap<>();
private final List<PendingServlet> pendingServlets = new ArrayList<>();
/** The bundle context. */
private volatile BundleContext 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 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 ------------------------------------
// ---------- 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),
DEFAULT_ERROR_HANDLER_RESOURCE_TYPE, 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(),
DEFAULT_ERROR_HANDLER_RESOURCE_TYPE, 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<>();
/**
* @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(),
DEFAULT_ERROR_HANDLER_RESOURCE_TYPE);
}
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) {
// use local variable to avoid race condition with activate
final ResolutionCache localCache = this.resolutionCache;
final Servlet scriptServlet = localCache.get(locationUtil);
if (scriptServlet != null) {
if ( LOGGER.isDebugEnabled() ) {
LOGGER.debug("Using cached servlet {}", RequestUtil.getServletName(scriptServlet));
}
return scriptServlet;
}
final Collection<Resource> candidates = locationUtil.getServlets(resolver, scriptEnginesExtensions);
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 ) {
localCache.put(locationUtil, candidate);
}
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_METHOD,
DEFAULT_ERROR_HANDLER_RESOURCE_TYPE, 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;
}
}
// ---------- SCR Integration ----------------------------------------------
/**
* Activate this component.
*/
@Activate
protected void activate(final BundleContext context, final Config config) throws LoginException {
final Collection<PendingServlet> refs;
synchronized (this.pendingServlets) {
refs = new ArrayList<>(pendingServlets);
pendingServlets.clear();
this.sharedScriptResolver =
resourceResolverFactory.getServiceResourceResolver(Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object)"scripts"));
this.searchPaths = this.sharedScriptResolver.getSearchPath();
servletResourceProviderFactory = new ServletResourceProviderFactory(config.servletresolver_servletRoot(), this.searchPaths);
// register servlets immediately from now on
this.context = context;
}
createAllServlets(refs);
this.executionPaths = AbstractResourceCollector.getExecutionPaths(config.servletresolver_paths());
this.defaultExtensions = config.servletresolver_defaultExtensions();
// setup default servlet
this.getDefaultServlet();
this.plugin = new ServletResolverWebConsolePlugin(context);
}
/**
* Deactivate this component.
*/
@Deactivate
protected void deactivate() {
// stop registering of servlets immediately
this.context = null;
if (this.plugin != null) {
this.plugin.dispose();
}
// Copy the list of servlets first, to minimize the need for
// synchronization
final Collection<ServiceReference<Servlet>> refs;
synchronized (this.servletsByReference) {
refs = new ArrayList<>(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.servletResourceProviderFactory = null;
}
// TODO
// This can be simplified once we can use DS from R7 with constructor injection
// as we can inject the bundle context through the constructor
@Reference(
name = REF_SERVLET,
service = Servlet.class,
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC,
target="(|(" + ServletResolverConstants.SLING_SERVLET_PATHS + "=*)(" + ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES + "=*))")
protected void bindServlet(final Servlet servlet, final ServiceReference<Servlet> reference) {
boolean directCreate = true;
if (context == null) {
synchronized ( pendingServlets ) {
if (context == null) {
pendingServlets.add(new PendingServlet(servlet, reference));
directCreate = false;
}
}
}
if ( directCreate ) {
createServlet(servlet, reference);
}
}
protected void unbindServlet(final ServiceReference<Servlet> reference) {
synchronized ( pendingServlets ) {
final Iterator<PendingServlet> iter = pendingServlets.iterator();
while ( iter.hasNext() ) {
final PendingServlet ps = iter.next();
if ( ps.reference.compareTo(reference) == 0 ) {
iter.remove();
break;
}
}
}
destroyServlet(reference);
}
// ---------- Servlet Management -------------------------------------------
private void createAllServlets(final Collection<PendingServlet> pendingServlets) {
for (final PendingServlet ps : pendingServlets) {
createServlet(ps.servlet, ps.reference);
}
}
private boolean createServlet(final Servlet servlet, final ServiceReference<Servlet> reference) {
// check for a name, this is required
final String name = getName(reference);
// check for Sling properties in the service registration
final ServletResourceProvider provider = servletResourceProviderFactory.create(reference, servlet);
if (provider == null) {
// this is expected if the servlet is not destined for Sling
return false;
}
// initialize now
try {
servlet.init(new SlingServletConfig(servletContext, reference, name));
LOGGER.debug("bindServlet: Servlet {} initialized", name);
} catch (final ServletException ce) {
LOGGER.error("bindServlet: Servlet " + ServletResourceProviderFactory.getServiceReferenceInfo(reference) + " failed to initialize", ce);
return false;
} catch (final Throwable t) {
LOGGER.error("bindServlet: Unexpected problem initializing servlet " + ServletResourceProviderFactory.getServiceReferenceInfo(reference), t);
return false;
}
boolean registered = false;
final Bundle bundle = reference.getBundle();
if ( bundle != null ) {
final BundleContext bundleContext = bundle.getBundleContext();
if ( bundleContext != null ) {
final List<ServiceRegistration<ResourceProvider<Object>>> regs = new ArrayList<>();
try {
for(final String root : provider.getServletPaths()) {
@SuppressWarnings("unchecked")
final ServiceRegistration<ResourceProvider<Object>> reg = (ServiceRegistration<ResourceProvider<Object>>) bundleContext.registerService(
ResourceProvider.class.getName(),
provider,
createServiceProperties(reference, provider, root));
regs.add(reg);
}
registered = true;
} catch ( final IllegalStateException ise ) {
// bundle context not valid anymore - ignore and continue without this
}
if ( registered ) {
if ( LOGGER.isDebugEnabled() ) {
LOGGER.debug("Registered {}", provider);
}
synchronized (this.servletsByReference) {
servletsByReference.put(reference, new ServletReg(servlet, regs));
}
}
}
}
if ( !registered ) {
LOGGER.debug("bindServlet: servlet has been unregistered in the meantime. Ignoring {}", name);
}
return true;
}
private Dictionary<String, Object> createServiceProperties(final ServiceReference<Servlet> reference,
final ServletResourceProvider provider,
final String root) {
final Dictionary<String, Object> params = new Hashtable<>();
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<Servlet>> refs) {
for (ServiceReference<Servlet> serviceReference : refs) {
destroyServlet(serviceReference);
}
}
private void destroyServlet(final ServiceReference<Servlet> reference) {
ServletReg registration;
synchronized (this.servletsByReference) {
registration = servletsByReference.remove(reference);
}
if (registration != null) {
for(final ServiceRegistration<ResourceProvider<Object>> reg : registration.registrations) {
try {
reg.unregister();
} catch ( final IllegalStateException ise) {
// this might happen on shutdown
}
}
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);
}
}
}
/** The list of property names checked by {@link #getName(ServiceReference)} */
private static final String[] NAME_PROPERTIES = { SLING_SERVLET_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.
* @return The servlet name. This method never returns {@code null}
*/
private static String getName(final ServiceReference<Servlet> 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<ResourceProvider<Object>>> registrations;
public ServletReg(final Servlet s, final List<ServiceRegistration<ResourceProvider<Object>>> srs) {
this.servlet = s;
this.registrations = srs;
}
}
private static final class PendingServlet {
public final Servlet servlet;
public final ServiceReference<Servlet> reference;
public PendingServlet(final Servlet s, final ServiceReference<Servlet> ref) {
this.servlet = s;
this.reference = ref;
}
}
@SuppressWarnings("serial")
class ServletResolverWebConsolePlugin extends HttpServlet {
private static final String PARAMETER_URL = "url";
private static final String PARAMETER_METHOD = "method";
private ServiceRegistration<Servlet> service;
public ServletResolverWebConsolePlugin(final BundleContext context) {
Dictionary<String, Object> props = new Hashtable<>();
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(Servlet.class, 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.getServiceResourceResolver(Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object)"console"));
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, scriptEnginesExtensions);
}
tr(pw);
tdLabel(pw, "Candidates");
tdContent(pw);
if (servlets == null || servlets.isEmpty()) {
pw.println("Could not find a suitable servlet for this request!");
} else {
// check for non-existing resources
if (ResourceUtil.isNonExistingResource(resource)) {
pw.println("The resource given by path '");
pw.println(resource.getPath());
pw.println("' does not exist. Therefore no resource type could be determined!<br/>");
}
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);
}
}
}
}