blob: 3562f7c01ae2ad4e9c4186bf562027bb95ecfd57 [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.engine.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
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.References;
import org.apache.sling.api.adapter.AdapterManager;
import org.apache.sling.api.request.SlingRequestEvent;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.ServletResolver;
import org.apache.sling.auth.core.AuthenticationSupport;
import org.apache.sling.commons.mime.MimeTypeService;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.engine.SlingRequestProcessor;
import org.apache.sling.engine.impl.filter.ServletFilterManager;
import org.apache.sling.engine.impl.helper.RequestListenerManager;
import org.apache.sling.engine.impl.helper.SlingServletContext;
import org.apache.sling.engine.impl.helper.SlingServletContext3;
import org.apache.sling.engine.impl.request.RequestData;
import org.apache.sling.engine.impl.request.RequestHistoryConsolePlugin;
import org.apache.sling.engine.jmx.RequestProcessorMBean;
import org.apache.sling.engine.servlets.ErrorHandler;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.Version;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>SlingMainServlet</code> TODO
*/
@SuppressWarnings("serial")
@Component(immediate = true, metatype = true, label = "%sling.name", description = "%sling.description")
@Properties( {
@Property(name = Constants.SERVICE_VENDOR, value = "The Apache Software Foundation"),
@Property(name = Constants.SERVICE_DESCRIPTION, value = "Sling Servlet")
})
@References( {
@Reference(name = "ErrorHandler", referenceInterface = ErrorHandler.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC, bind = "setErrorHandler", unbind = "unsetErrorHandler"),
@Reference(name = "ServletResolver", referenceInterface = ServletResolver.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC, bind = "setServletResolver", unbind = "unsetServletResolver"),
@Reference(name = "MimeTypeService", referenceInterface = MimeTypeService.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC, bind = "setMimeTypeService", unbind = "unsetMimeTypeService"),
@Reference(name = "AuthenticationSupport", referenceInterface = AuthenticationSupport.class, cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC, bind = "setAuthenticationSupport", unbind = "unsetAuthenticationSupport") })
public class SlingMainServlet extends GenericServlet {
@Property(intValue=RequestData.DEFAULT_MAX_CALL_COUNTER)
public static final String PROP_MAX_CALL_COUNTER = "sling.max.calls";
@Property(intValue=RequestData.DEFAULT_MAX_INCLUSION_COUNTER)
public static final String PROP_MAX_INCLUSION_COUNTER = "sling.max.inclusions";
public static final boolean DEFAULT_ALLOW_TRACE = false;
@Property(boolValue=DEFAULT_ALLOW_TRACE)
public static final String PROP_ALLOW_TRACE = "sling.trace.allow";
public static final boolean DEFAULT_FILTER_COMPAT_MODE = false;
@Property(boolValue=DEFAULT_FILTER_COMPAT_MODE)
public static final String PROP_FILTER_COMPAT_MODE = "sling.filter.compat.mode";
@Property(intValue = RequestHistoryConsolePlugin.STORED_REQUESTS_COUNT)
private static final String PROP_MAX_RECORD_REQUESTS = "sling.max.record.requests";
@Property(unbounded=PropertyUnbounded.ARRAY)
private static final String PROP_TRACK_PATTERNS_REQUESTS = "sling.store.pattern.requests";
private static final String PROP_DEFAULT_PARAMETER_ENCODING = "sling.default.parameter.encoding";
@Property
private static final String PROP_SERVER_INFO = "sling.serverinfo";
@Reference
private HttpService httpService;
@Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC)
private AdapterManager adapterManager;
/** default log */
private final Logger log = LoggerFactory.getLogger(SlingMainServlet.class);
/**
* The registration path for the SlingMainServlet is hard wired to always
* be the root, aka "<code>/</code>" (value is "/").
*/
private static final String SLING_ROOT = "/";
/**
* The name of the product to report in the {@link #getServerInfo()} method
* (value is "ApacheSling").
*/
static String PRODUCT_NAME = "ApacheSling";
private SlingServletContext slingServletContext;
/**
* The product information part of the {@link #serverInfo} returns from the
* <code>ServletContext.getServerInfo()</code> method. This field defaults
* to {@link #PRODUCT_NAME} and is amended with the major and minor version
* of the Sling Engine bundle while this component is being
* {@link #activate(BundleContext, Map)} activated}.
*/
private String productInfo = PRODUCT_NAME;
/**
* The server information to report in the {@link #getServerInfo()} method.
* By default this is just the {@link #PRODUCT_NAME} (same as
* {@link #productInfo}. During {@link #activate(BundleContext, Map)}
* activation} the field is updated with the full {@link #productInfo} value
* as well as the operating system and java version it is running on.
* Finally during servlet initialization the product information from the
* servlet container's server info is added to the comment section.
*/
private String serverInfo = PRODUCT_NAME;
private RequestListenerManager requestListenerManager;
private boolean allowTrace = DEFAULT_ALLOW_TRACE;
private Object printerRegistration;
// new properties
private SlingHttpContext slingHttpContext = new SlingHttpContext();
private ServletFilterManager filterManager;
private final SlingRequestProcessorImpl requestProcessor = new SlingRequestProcessorImpl();
private ServiceRegistration requestProcessorRegistration;
private ServiceRegistration requestProcessorMBeanRegistration;
private String configuredServerInfo;
// ---------- Servlet API -------------------------------------------------
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException {
if (req instanceof HttpServletRequest
&& res instanceof HttpServletResponse) {
HttpServletRequest request = (HttpServletRequest) req;
// set the thread name according to the request
String threadName = setThreadName(request);
requestListenerManager.sendEvent( request, SlingRequestEvent.EventType.EVENT_INIT );
ResourceResolver resolver = null;
try {
if (!allowTrace && "TRACE".equals(request.getMethod())) {
HttpServletResponse response = (HttpServletResponse) res;
response.setStatus(405);
response.setHeader("Allow", "GET, HEAD, POST, PUT, DELETE, OPTIONS");
return;
}
// get ResourceResolver (set by AuthenticationSupport)
Object resolverObject = request.getAttribute(AuthenticationSupport.REQUEST_ATTRIBUTE_RESOLVER);
resolver = (resolverObject instanceof ResourceResolver)
? (ResourceResolver) resolverObject
: null;
// real request handling for HTTP requests
requestProcessor.doProcessRequest(request, (HttpServletResponse) res,
resolver);
} catch (IOException ioe) {
// SLING-3498: Jetty with NIO does not have a wrapped
// SocketException any longer but a plain IOException
// from the NIO Socket channel. Hence we don't care for
// unwrapping and just log at DEBUG level
log.debug("service: Probably client aborted request or any other network problem", ioe);
} catch (Throwable t) {
// some failure while handling the request, log the issue
// and terminate. We do not call error handling here, because
// we assume the real request handling would have done this.
// So here we just log
log.error("service: Uncaught Problem handling the request", t);
} finally {
// close the resource resolver (not relying on servlet request
// listener to do this for now; see SLING-1270)
if (resolver != null) {
resolver.close();
}
requestListenerManager.sendEvent( request, SlingRequestEvent.EventType.EVENT_DESTROY );
// reset the thread name
if (threadName != null) {
Thread.currentThread().setName(threadName);
}
}
} else {
throw new ServletException(
"Apache Sling must be run in an HTTP servlet environment.");
}
}
// ---------- Internal helper ----------------------------------------------
/**
* Sets the {@link #productInfo} field from the providing bundle's version
* and the {@link #PRODUCT_NAME}.
* <p>
* Also {@link #setServerInfo() updates} the {@link #serverInfo} based
* on the product info calculated.
*
* @param bundleContext Provides access to the "Bundle-Version" manifest
* header of the containing bundle.
*/
private void setProductInfo(final BundleContext bundleContext) {
final Dictionary<?, ?> props = bundleContext.getBundle().getHeaders();
final Version bundleVersion = Version.parseVersion((String) props.get(Constants.BUNDLE_VERSION));
final String productVersion = bundleVersion.getMajor() + "."
+ bundleVersion.getMinor();
this.productInfo = PRODUCT_NAME + "/" + productVersion;
// update the server info
this.setServerInfo();
}
public String getServerInfo() {
return serverInfo;
}
/**
* Sets up the server info to be returned for the
* <code>ServletContext.getServerInfo()</code> method for servlets and
* filters deployed inside Sling. The {@link SlingRequestProcessor} instance
* is also updated with the server information.
* <p>
* This server info is either configured through an OSGi configuration or
* it is made up of the following components:
* <ol>
* <li>The {@link #productInfo} field as the primary product information</li>
* <li>The primary product information of the servlet container into which
* the Sling Main Servlet is deployed. If the servlet has not yet been
* deployed this will show as <i>unregistered</i>. If the servlet container
* does not provide a server info this will show as <i>unknown</i>.</li>
* <li>The name and version of the Java VM as reported by the
* <code>java.vm.name</code> and <code>java.vm.version</code> system
* properties</li>
* <li>The name, version, and architecture of the OS platform as reported by
* the <code>os.name</code>, <code>os.version</code>, and
* <code>os.arch</code> system properties</li>
* </ol>
*/
private void setServerInfo() {
if ( this.configuredServerInfo != null ) {
this.serverInfo = this.configuredServerInfo;
} else {
final String containerProductInfo;
if (getServletConfig() == null || getServletContext() == null) {
containerProductInfo = "unregistered";
} else {
final String containerInfo = getServletContext().getServerInfo();
if (containerInfo != null && containerInfo.length() > 0) {
int lbrace = containerInfo.indexOf('(');
if (lbrace < 0) {
lbrace = containerInfo.length();
}
containerProductInfo = containerInfo.substring(0, lbrace).trim();
} else {
containerProductInfo = "unknown";
}
}
this.serverInfo = String.format("%s (%s, %s %s, %s %s %s)",
this.productInfo, containerProductInfo,
System.getProperty("java.vm.name"),
System.getProperty("java.version"), System.getProperty("os.name"),
System.getProperty("os.version"), System.getProperty("os.arch"));
}
if (this.requestProcessor != null) {
this.requestProcessor.setServerInfo(serverInfo);
}
}
// ---------- Property Setter for SCR --------------------------------------
@Activate
protected void activate(final BundleContext bundleContext,
final Map<String, Object> componentConfig) {
configuredServerInfo = PropertiesUtil.toString(componentConfig.get(PROP_SERVER_INFO), null);
// setup server info
setProductInfo(bundleContext);
// prepare the servlet configuration from the component config
final Hashtable<String, Object> configuration = new Hashtable<String, Object>(
componentConfig);
// ensure the servlet name
if (!(configuration.get("servlet-name") instanceof String)) {
configuration.put("servlet-name", this.productInfo);
}
// configure method filter
allowTrace = PropertiesUtil.toBoolean(componentConfig.get(PROP_ALLOW_TRACE),
DEFAULT_ALLOW_TRACE);
// configure the request limits
RequestData.setMaxIncludeCounter(PropertiesUtil.toInteger(
componentConfig.get(PROP_MAX_INCLUSION_COUNTER),
RequestData.DEFAULT_MAX_INCLUSION_COUNTER));
RequestData.setMaxCallCounter(PropertiesUtil.toInteger(
componentConfig.get(PROP_MAX_CALL_COUNTER),
RequestData.DEFAULT_MAX_CALL_COUNTER));
RequestData.setSlingMainServlet(this);
// configure default request parameter encoding
// log a message if such configuration exists ....
if (componentConfig.get(PROP_DEFAULT_PARAMETER_ENCODING) != null) {
log.warn("Configure default request parameter encoding with 'org.apache.sling.parameters.config' configuration; the property "
+ PROP_DEFAULT_PARAMETER_ENCODING
+ "="
+ componentConfig.get(PROP_DEFAULT_PARAMETER_ENCODING)
+ " is ignored");
}
// register the servlet and resources
try {
Dictionary<String, String> servletConfig = toStringConfig(configuration);
this.httpService.registerServlet(SLING_ROOT, this, servletConfig,
slingHttpContext);
log.info("{} ready to serve requests", this.getServerInfo());
} catch (Exception e) {
log.error("Cannot register " + this.getServerInfo(), e);
}
// now that the sling main servlet is registered with the HttpService
// and initialized we can register the servlet context
if (getServletContext() == null || getServletContext().getMajorVersion() < 3) {
slingServletContext = new SlingServletContext(bundleContext, this);
} else {
slingServletContext = new SlingServletContext3(bundleContext, this);
}
// register render filters already registered after registration with
// the HttpService as filter initialization may cause the servlet
// context to be required (see SLING-42)
filterManager = new ServletFilterManager(bundleContext,
slingServletContext,
PropertiesUtil.toBoolean(componentConfig.get(PROP_FILTER_COMPAT_MODE), DEFAULT_FILTER_COMPAT_MODE));
filterManager.open();
requestProcessor.setFilterManager(filterManager);
// initialize requestListenerManager
requestListenerManager = new RequestListenerManager( bundleContext, slingServletContext );
// Setup configuration printer
this.printerRegistration = WebConsoleConfigPrinter.register(bundleContext, filterManager);
// setup the request info recorder
try {
int maxRequests = PropertiesUtil.toInteger(
componentConfig.get(PROP_MAX_RECORD_REQUESTS),
RequestHistoryConsolePlugin.STORED_REQUESTS_COUNT);
String[] patterns = PropertiesUtil.toStringArray(componentConfig.get(PROP_TRACK_PATTERNS_REQUESTS), new String[0]);
List<Pattern> compiledPatterns = new ArrayList<Pattern>(patterns.length);
for (String pattern : patterns) {
if(pattern != null && pattern.trim().length() > 0) {
compiledPatterns.add(Pattern.compile(pattern));
}
}
RequestHistoryConsolePlugin.initPlugin(bundleContext, maxRequests, compiledPatterns);
} catch (Throwable t) {
log.debug(
"Unable to register web console request recorder plugin.", t);
}
try {
Dictionary<String, String> mbeanProps = new Hashtable<String, String>();
mbeanProps.put("jmx.objectname", "org.apache.sling:type=engine,service=RequestProcessor");
RequestProcessorMBeanImpl mbean = new RequestProcessorMBeanImpl();
requestProcessorMBeanRegistration = bundleContext.registerService(RequestProcessorMBean.class.getName(), mbean, mbeanProps);
requestProcessor.setMBean(mbean);
} catch (Throwable t) {
log.debug("Unable to register mbean");
}
// provide the SlingRequestProcessor service
Hashtable<String, String> srpProps = new Hashtable<String, String>();
srpProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
srpProps.put(Constants.SERVICE_DESCRIPTION, "Sling Request Processor");
requestProcessorRegistration = bundleContext.registerService(
SlingRequestProcessor.NAME, requestProcessor, srpProps);
}
@Override
public void init() {
setServerInfo();
}
@Deactivate
protected void deactivate() {
// unregister the sling request processor
if (requestProcessorRegistration != null) {
requestProcessorRegistration.unregister();
requestProcessorRegistration = null;
}
if (requestProcessorMBeanRegistration != null) {
requestProcessorMBeanRegistration.unregister();
requestProcessorMBeanRegistration = null;
}
// unregister request recorder plugin
try {
RequestHistoryConsolePlugin.destroyPlugin();
} catch (Throwable t) {
log.debug(
"Problem unregistering web console request recorder plugin.", t);
}
// this reverses the activation setup
if ( this.printerRegistration != null ) {
WebConsoleConfigPrinter.unregister(this.printerRegistration);
this.printerRegistration = null;
}
// destroy servlet filters before destroying the sling servlet
// context because the filters depend on that context
if (filterManager != null) {
requestProcessor.setFilterManager(null);
filterManager.close();
}
// second unregister the servlet context *before* unregistering
// and destroying the the sling main servlet
if (slingServletContext != null) {
slingServletContext.dispose();
slingServletContext = null;
}
// third unregister and destroy the sling main servlet
httpService.unregister(SLING_ROOT);
// dispose of request listener manager after unregistering the servlet
// to prevent a potential NPE in the service method
if ( this.requestListenerManager != null ) {
this.requestListenerManager.dispose();
this.requestListenerManager = null;
}
// reset the sling main servlet reference (help GC and be nice)
RequestData.setSlingMainServlet(null);
log.info(this.getServerInfo() + " shut down");
}
void setErrorHandler(final ErrorHandler errorHandler) {
requestProcessor.setErrorHandler(errorHandler);
}
void unsetErrorHandler(final ErrorHandler errorHandler) {
requestProcessor.unsetErrorHandler(errorHandler);
}
public void setServletResolver(final ServletResolver servletResolver) {
requestProcessor.setServletResolver(servletResolver);
}
public void unsetServletResolver(final ServletResolver servletResolver) {
requestProcessor.unsetServletResolver(servletResolver);
}
public void setMimeTypeService(final MimeTypeService mimeTypeService) {
slingHttpContext.setMimeTypeService(mimeTypeService);
}
public void unsetMimeTypeService(final MimeTypeService mimeTypeService) {
slingHttpContext.unsetMimeTypeService(mimeTypeService);
}
public void setAuthenticationSupport(
final AuthenticationSupport authenticationSupport) {
slingHttpContext.setAuthenticationSupport(authenticationSupport);
}
public void unsetAuthenticationSupport(
final AuthenticationSupport authenticationSupport) {
slingHttpContext.unsetAuthenticationSupport(authenticationSupport);
}
private Dictionary<String, String> toStringConfig(Dictionary<?, ?> config) {
Dictionary<String, String> stringConfig = new Hashtable<String, String>();
for (Enumeration<?> ke = config.keys(); ke.hasMoreElements();) {
Object key = ke.nextElement();
stringConfig.put(key.toString(), String.valueOf(config.get(key)));
}
return stringConfig;
}
// ---------- HttpContext interface ----------------------------------------
public String getMimeType(String name) {
return slingHttpContext.getMimeType(name);
}
public <Type> Type adaptTo(Object object, Class<Type> type) {
AdapterManager adapterManager = this.adapterManager;
if (adapterManager != null) {
return adapterManager.getAdapter(object, type);
}
// no adapter manager, nothing to adapt to
return null;
}
/**
* Sets the name of the current thread to the IP address of the remote
* client with the current system time and the first request line consisting
* of the method, path and protocol.
*
* @param request The request to extract the remote IP address, method,
* request URL and protocol from.
* @return The name of the current thread before setting the new name.
*/
private String setThreadName(HttpServletRequest request) {
// get the name of the current thread (to be returned)
Thread thread = Thread.currentThread();
String oldThreadName = thread.getName();
// construct and set the new thread name of the form:
// 127.0.0.1 [1224156108055] GET /system/console/config HTTP/1.1
final StringBuilder buf = new StringBuilder();
buf.append(request.getRemoteAddr());
buf.append(" [").append(System.currentTimeMillis()).append("] ");
buf.append(request.getMethod()).append(' ');
buf.append(request.getRequestURI()).append(' ');
buf.append(request.getProtocol());
thread.setName(buf.toString());
// return the previous thread name
return oldThreadName;
}
}