blob: 7a56be1145f3adcee6b967a3e21a130462564d34 [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.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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.RequestProgressTracker;
import org.apache.sling.api.request.RequestUtil;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceProvider;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
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.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.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.
*
* @scr.component name="org.apache.sling.servlets.resolver.SlingServletResolver"
* label="%servletresolver.name"
* description="%servletresolver.description"
* @scr.property name="service.description" value="Sling Servlet Resolver and
* Error Handler"
* @scr.property name="service.vendor" value="The Apache Software Foundation"
* @scr.service interface="ServletResolver"
* @scr.service interface="SlingScriptResolver"
* @scr.service interface="ErrorHandler"
* @scr.reference name="Servlet" interface="javax.servlet.Servlet"
* cardinality="0..n" policy="dynamic"
* @scr.property name="event.topics"
* values.1="org/apache/sling/api/resource/Resource/*"
* values.2="org/apache/sling/api/resource/ResourceProvider/*"
* values.3="javax/script/ScriptEngineFactory/*"
* values.4="org/apache/sling/api/adapter/AdapterFactory/*"
* private="true"
*/
public class SlingServletResolver implements ServletResolver, SlingScriptResolver, ErrorHandler, EventHandler {
/** Servlet resolver logger */
public static final Logger LOGGER = LoggerFactory.getLogger(SlingServletResolver.class);
/**
* @scr.property valueRef="DEFAULT_SERVLET_ROOT"
*/
public static final String PROP_SERVLET_ROOT = "servletresolver.servletRoot";
/**
* @scr.property
*/
public static final String PROP_SCRIPT_USER = "servletresolver.scriptUser";
/**
* @scr.property valueRef="DEFAULT_CACHE_SIZE"
*/
public static final String PROP_CACHE_SIZE = "servletresolver.cacheSize";
/**
* @scr.property
*/
public static final String PROP_DEFAULT_SCRIPT_WORKSPACE = "servletresolver.defaultScriptWorkspace";
/**
* @scr.property valueRef="DEFAULT_USE_REQUEST_WORKSPACE"
*/
public static final String PROP_USE_REQUEST_WORKSPACE = "servletresolver.useRequestWorkspace";
private static final boolean DEFAULT_USE_REQUEST_WORKSPACE = false;
/**
* @scr.property valueRef="DEFAULT_USE_DEFAULT_WORKSPACE"
*/
public static final String PROP_USE_DEFAULT_WORKSPACE = "servletresolver.useDefaultWorkspace";
private static final boolean DEFAULT_USE_DEFAULT_WORKSPACE = false;
/**
* 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 Integer DEFAULT_CACHE_SIZE = 200;
private static final String REF_SERVLET = "Servlet";
/**
* @scr.property values="/"
*/
public static final String PROP_PATHS = "servletresolver.paths";
private static final String[] DEFAULT_PATHS = new String[] {"/"};
/**
* @scr.property values="html"
*/
public static final String PROP_DEFAULT_EXTENSIONS = "servletresolver.defaultExtensions";
private static final String[] DEFAULT_DEFAULT_EXTENSIONS = new String[] {"html"};
/** @scr.reference */
private ServletContext servletContext;
/** @scr.reference */
private ResourceResolverFactory resourceResolverFactory;
private ResourceResolver scriptResolver;
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;
/**
* If true, the primary workspace name for script resolution will be the
* same as that used to resolve the request's resource.
*/
private boolean useRequestWorkspace;
/**
* If true and useRequestWorkspace is true and no scripts are found using
* the request workspace, also use the default workspace. If
* useRequestWorkspace is false, this value is ignored.
*/
private boolean useDefaultWorkspace;
/**
* The default workspace to use (might be null to use the default
* workspace).
*/
private String defaultWorkspaceName;
/**
* The allowed execution paths.
*/
private String[] executionPaths;
/**
* The default extensions
*/
private String[] defaultExtensions;
// ---------- ServletResolver interface -----------------------------------
/**
* @see org.apache.sling.api.servlets.ServletResolver#resolveServlet(org.apache.sling.api.SlingHttpServletRequest)
*/
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 + ")";
tracker.startTimer(timerName);
final String type = resource.getResourceType();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("resolveServlet called for resource {}", resource);
}
Servlet servlet = null;
if (this.useRequestWorkspace) {
final String wspName = getWorkspaceName(request);
// First, we use a resource resolver using the same workspace as the
// resource
servlet = resolveServlet(request, type, scriptResolver, wspName);
// now we try the default workspace
if (servlet == null && this.useDefaultWorkspace && wspName != null ) {
servlet = resolveServlet(request, type, scriptResolver, this.defaultWorkspaceName);
}
} else {
servlet = resolveServlet(request, type, scriptResolver, this.defaultWorkspaceName);
}
// 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)
*/
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 Servlet servlet = resolveServlet(scriptResolver, resource, scriptName);
// 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)
*/
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 Servlet servlet = resolveServlet(scriptResolver, null, scriptName);
// 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;
}
/** Internal method to resolve a servlet. */
private Servlet resolveServlet(final ResourceResolver resolver,
final Resource resource,
final String scriptName) {
Servlet servlet = null;
// first check whether the type of a resource is the absolute
// path of a servlet (or script)
if (scriptName.charAt(0) == '/') {
if ( this.isPathAllowed(scriptName) ) {
final Resource res = resolver.getResource(scriptName);
if (res != null) {
servlet = res.adaptTo(Servlet.class);
}
if (servlet != null && LOGGER.isDebugEnabled()) {
LOGGER.debug("Servlet {} found using absolute resource type {}", RequestUtil.getServletName(servlet),
scriptName);
}
}
}
if ( servlet == null ) {
// the resource type is not absolute, so lets go for the deep search
final NamedScriptResourceCollector locationUtil = NamedScriptResourceCollector.create(scriptName, resource, this.executionPaths);
servlet = getServlet(locationUtil, null, resolver);
if (LOGGER.isDebugEnabled() && servlet != null) {
LOGGER.debug("resolveServlet returns servlet {}", RequestUtil.getServletName(servlet));
}
}
return servlet;
}
// ---------- ScriptResolver interface ------------------------------------
/**
* @see org.apache.sling.api.scripting.SlingScriptResolver#findScript(org.apache.sling.api.resource.ResourceResolver, java.lang.String)
*/
public SlingScript findScript(final ResourceResolver resourceResolver, final String name)
throws SlingException {
// is the path absolute
SlingScript script = null;
if (name.startsWith("/")) {
if ( this.isPathAllowed(name) ) {
final Resource resource = resourceResolver.getResource(name);
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 = 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)
*/
public void handleError(int status, String message, SlingHttpServletRequest request,
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);
try {
final String wspName = (this.useRequestWorkspace ? getWorkspaceName(request) : null);
// 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, wspName,
this.executionPaths);
Servlet servlet = getServlet(locationUtil, request, scriptResolver);
// fall back to default servlet if none
if (servlet == null) {
servlet = getDefaultErrorServlet(request, scriptResolver,
resource, wspName);
}
// 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");
}
}
public void handleError(Throwable throwable, SlingHttpServletRequest request, 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);
try {
final String wspName = (this.useRequestWorkspace ? getWorkspaceName(request) : null);
// 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, wspName,
this.executionPaths);
servlet = getServlet(locationUtil, request, scriptResolver);
// go to the base class
tClass = tClass.getSuperclass();
}
if (servlet == null) {
servlet = getDefaultErrorServlet(request, scriptResolver,
resource, wspName);
}
// 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 ---------------------------------------------
/**
* 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(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 and workspace
*/
private Servlet resolveServlet(final SlingHttpServletRequest request,
final String type,
final ResourceResolver resolver,
final String workspaceName) {
Servlet servlet = null;
// first check whether the type of a resource is the absolute
// path of a servlet (or script)
if (type.charAt(0) == '/') {
if ( this.isPathAllowed(type) ) {
String path = type;
if ( workspaceName != null ) {
path = workspaceName + ':' + type;
}
final Resource res = resolver.getResource(path);
if (res != null) {
servlet = res.adaptTo(Servlet.class);
}
if (servlet != null && LOGGER.isDebugEnabled()) {
LOGGER.debug("Servlet {} found using absolute resource type {}", RequestUtil.getServletName(servlet),
type);
}
}
}
if ( servlet == null ) {
// the resource type is not absolute, so lets go for the deep search
final ResourceCollector locationUtil = ResourceCollector.create(request, workspaceName, this.executionPaths, this.defaultExtensions);
servlet = getServlet(locationUtil, request, resolver);
if (servlet != null && LOGGER.isDebugEnabled()) {
LOGGER.debug("getServlet 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 resource The <code>Resource</code> for which to find a script.
* This need not be the same as
* <code>request.getResource()</code> in case of error handling
* where the resource may not have been assigned to the request
* yet.
* @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 getServlet(final AbstractResourceCollector locationUtil,
final SlingHttpServletRequest request,
final ResourceResolver scriptResolver) {
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(scriptResolver);
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 (Resource candidateResource : candidates) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Checking if candidate resource {} adapts to servlet and accepts request", candidateResource
.getPath());
}
Servlet candidate = candidateResource.adaptTo(Servlet.class);
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;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Candidate {} does not accept request, ignored", candidateResource.getPath());
}
} else {
if (LOGGER.isDebugEnabled()) {
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, "Sling Core Default Servlet"));
defaultServlet = servlet;
} catch (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 ResourceResolver scriptResolver,
final Resource resource,
final String workspaceName) {
// 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,
workspaceName,
this.executionPaths);
final Servlet servlet = getServlet(locationUtil, request,
scriptResolver);
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(Servlet errorHandler, HttpServletRequest request, 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());
}
try {
errorHandler.service(request, response);
} catch (IOException ioe) {
// forware the IOException
throw ioe;
} catch (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));
}
}
/**
* Package scoped to help with testing.
*/
String getWorkspaceName(SlingHttpServletRequest request) {
if ( this.useRequestWorkspace ) {
final String path = request.getResource().getPath();
final int pos = path.indexOf(":/");
if ( pos == -1 ) {
return null; // default workspace
}
return path.substring(0, pos);
}
return null;
}
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;
}
// workspace handling and resource resolver creation
this.useDefaultWorkspace = OsgiUtil.toBoolean(properties.get(PROP_USE_DEFAULT_WORKSPACE), DEFAULT_USE_DEFAULT_WORKSPACE);
this.useRequestWorkspace = OsgiUtil.toBoolean(properties.get(PROP_USE_REQUEST_WORKSPACE), DEFAULT_USE_REQUEST_WORKSPACE);
String defaultWorkspaceProp = (String) properties.get(PROP_DEFAULT_SCRIPT_WORKSPACE);
if ( defaultWorkspaceProp != null && defaultWorkspaceProp.trim().length() == 0 ) {
defaultWorkspaceProp = null;
}
this.defaultWorkspaceName = defaultWorkspaceProp;
final Collection<ServiceReference> refs;
synchronized (this.pendingServlets) {
refs = new ArrayList<ServiceReference>(pendingServlets);
pendingServlets.clear();
this.scriptResolver =
resourceResolverFactory.getAdministrativeResourceResolver(this.createAuthenticationInfo(context.getProperties()));
servletResourceProviderFactory = new ServletResourceProviderFactory(servletRoot,
this.scriptResolver.getSearchPath());
// 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;
}
}
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;
}
// and finally register as event listener
this.eventHandlerReg = context.getBundleContext().registerService(EventHandler.class.getName(), this,
properties);
}
/**
* Deactivate this component.
*/
protected void deactivate(final ComponentContext context) {
// stop registering of servlets immediately
this.context = null;
// 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.scriptResolver != null) {
this.scriptResolver.close();
this.scriptResolver = null;
}
this.cache = null;
this.servletResourceProviderFactory = null;
}
protected void bindServlet(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(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 Dictionary<String, Object> params = new Hashtable<String, Object>();
params.put(ResourceProvider.ROOTS, provider.getServletPaths());
params.put(Constants.SERVICE_DESCRIPTION, "ServletResourceProvider for Servlets at "
+ Arrays.asList(provider.getServletPaths()));
final ServiceRegistration reg = context.getBundleContext()
.registerService(ResourceProvider.SERVICE_NAME, provider, params);
LOGGER.info("Registered {}", provider.toString());
synchronized (this.servletsByReference) {
servletsByReference.put(reference, new ServletReg(servlet, reg));
}
return true;
}
private void destroyAllServlets(Collection<ServiceReference> refs) {
for (ServiceReference serviceReference : refs) {
destroyServlet(serviceReference);
}
}
private void destroyServlet(ServiceReference reference) {
ServletReg registration;
synchronized (this.servletsByReference) {
registration = servletsByReference.remove(reference);
}
if (registration != null) {
registration.registration.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)
*/
public void handleEvent(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 {
// this is a resource event
// if the path of the event is a sub path of a search path
// we flush the whole cache
String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH);
if (path.contains(":")) {
path = path.substring(path.indexOf(":") + 1);
}
final String[] searchPaths = this.scriptResolver.getSearchPath();
int index = 0;
while (!flushCache && index < searchPaths.length) {
if (path.startsWith(searchPaths[index])) {
flushCache = true;
}
index++;
}
}
if (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(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 ServiceRegistration registration;
public ServletReg(final Servlet s, final ServiceRegistration sr) {
this.servlet = s;
this.registration = sr;
}
}
}