/* vim: set ts=2 et sw=2 cindent fo=qroca: */
package com.globant.katari.core.web;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** This servlet is the entry point to all web requests for all modules.
*
* It dispatches all requests to another servlet based on the module name. This
* servlet gets all requests from the SpringBootstrapServlet, so that spring
* has a chance to inject all dependencies.
*
* The SpringBootstrapServlet is generally mapped to the 'module' path. The
* first path component after the servlet map is the module name. This servlet
* delegates the request to another servlet hidding all this module thing.
*/
public class ModuleContainerServlet extends HttpServlet {
/** The serialization version number.
*
* This number must change every time a new serialization incompatible change
* is introduced in the class.
*/
private static final long serialVersionUID = 20071005;
/** The class logger.
*/
private static Logger log = LoggerFactory.getLogger(
ModuleContainerServlet.class);
/** A map of module names to module configuration.
*
* A module configuration is simply another map that maps a url fragment to a
* servlet plus its configuration. It is never null.
*/
private Map<String, Map<String, ServletAndParameters>> modulesMap
= new HashMap<String, Map<String, ServletAndParameters>>();
/** Adds a module to the modulesMap.
*
* @param moduleName The module name. It cannot be null.
*
* @param servletAndParams The map of urls to servlets of the module. It
* cannot be null.
*/
public void addModule(final String moduleName,
final Map<String, ServletAndParameters> servletAndParams) {
Validate.notNull(moduleName, "The module name cannot be null");
Validate.notNull(servletAndParams, "The servlet and parameters cannot be"
+ " null");
modulesMap.put(moduleName, servletAndParams);
}
/** Called by the servlet container to indicate to a servlet that it is being
* placed into service.
*
* @param servletConfig The servlet's configuration and initialization
* parameters. This object is created by the container.
*
* @throws ServletException if an unexpected exception occurs.
*/
public void init(final ServletConfig servletConfig) throws ServletException {
log.trace("Entering init");
super.init(servletConfig);
for (Map.Entry<String, Map<String, ServletAndParameters>> entry
: modulesMap.entrySet()) {
String module = entry.getKey();
Map<String, ServletAndParameters> moduleMapping = entry.getValue();
initModule(servletConfig, module, moduleMapping);
}
log.trace("Leaving init");
}
/** Calls destroy on every configured servlet.
*/
public void destroy() {
log.trace("Entering destroy");
for (Map<String, ServletAndParameters> mapping : modulesMap.values()) {
for (ServletAndParameters servlet : mapping.values()) {
servlet.getServlet().destroy();
}
}
super.destroy();
log.trace("Leaving destroy");
}
/** Initializes all the servlets in a module.
*
* @param config The servlet configuration. It cannot be null.
*
* @param module The module name. It cannot be null.
*
* @param moduleMapping The url to servlet mapping corresponding to the
* module.
*
* @throws ServletException if an error occurs.
*/
private void initModule(final ServletConfig config, final String module,
final Map<String, ServletAndParameters> moduleMapping)
throws ServletException {
log.trace("Entering init");
Validate.notNull(config, "The servlet config cannot be null");
Validate.notNull(module, "The module name cannot be null");
Validate.notNull(moduleMapping, "The module mapping cannot be null");
List<HttpServlet> initializedServlets = new ArrayList<HttpServlet>();
for (Map.Entry<String, ServletAndParameters> entry
: moduleMapping.entrySet()) {
ServletAndParameters servletAndParameters;
servletAndParameters = (ServletAndParameters) entry.getValue();
HttpServlet servlet = servletAndParameters.getServlet();
if (!containsServlet(initializedServlets, servlet)) {
ModuleServletConfig moduleConfig = new ModuleServletConfig(this,
config.getServletContext(), module,
servletAndParameters.getParameters());
servlet.init(moduleConfig);
initializedServlets.add(servlet);
}
}
log.trace("Leaving init");
}
/**
* Looks for a servlet inside the list of servlets passed as parameter.
*
* It compares the servlets using the '==' operator instead of equals method
* in order to ensure that both are the same object.
* @param servlets the list of servlets we are looking into.
* @param servlet the servlet we are looking for.
* @return true if the servlets list contains the specified servlet, false
* otherwise.
*/
private boolean containsServlet(final List<HttpServlet> servlets,
final HttpServlet servlet) {
for (HttpServlet httpServlet : servlets) {
if (httpServlet == servlet) {
return true;
}
}
return false;
}
/** Called by the servlet container to allow the servlet to respond to a
* request.
*
* @param request The HttpServletRequest object that contains the client's
* request.
*
* @param response The HttpServletResponse object that contains the
* servlet's response
*
* @throws IOException if an input or output exception occurs.
*
* @throws ServletException if some other error occurs.
*/
protected void service(final HttpServletRequest request, final
HttpServletResponse response) throws ServletException, IOException {
log.trace("Entering service");
/* This is the info for a web.xml mapped struts servlet.
requestURL = http://localhost:8080/katari-i/welcome.do
requestURI = /katari-i/welcome.do
contextPath = /katari-i
servletPath = /welcome.do
pathInfo = null
*/
if (log.isDebugEnabled()) {
log.debug("requestURL = " + request.getRequestURL());
log.debug("requestURI = " + request.getRequestURI());
log.debug("contextPath = " + request.getContextPath());
log.debug("servletPath = " + request.getServletPath());
log.debug("pathInfo = " + request.getPathInfo());
log.debug("servletContextName = "
+ getServletContext().getServletContextName());
}
String pathInfo;
if (isIncluded(request)) {
pathInfo = (String) request.getAttribute(
"javax.servlet.include.path_info");
} else {
pathInfo = request.getPathInfo();
}
ServletData servletData = getServletFromUri(pathInfo);
ModuleRequestWrapper requestWrapper = new ModuleRequestWrapper(request,
servletData.getModuleName(), servletData.getServletPath());
Object savedRequest = request.getAttribute("request");
Object savedResponse = request.getAttribute("response");
servletData.getServlet().service(requestWrapper, response);
if (savedRequest != null) {
request.setAttribute("request", savedRequest);
}
if (savedResponse != null) {
request.setAttribute("response", savedResponse);
}
log.trace("Leaving service");
}
/** Obtains the servlet configuration from a url path fragment.
*
* @param path The url fragment that follows this servlet path.
*
* @return Returns all the data needed to forward the request to the
* correct servlet. It never returns null.
*/
public ServletData getServletFromUri(final String path) {
Validate.notNull(path, "The path cannot be null");
// Iterates over all the modules.
for (Map.Entry<String, Map<String, ServletAndParameters>> entry
: modulesMap.entrySet()) {
String module = entry.getKey();
if (path.startsWith("/" + module + "/") || path.equals("/"
+ module)) {
// Found the module, now iterate over the servlets.
if (log.isDebugEnabled()) {
log.debug("Dispatching request to module: " + module);
}
Map<String, ServletAndParameters> moduleMapping = entry.getValue();
String modulePath = path.substring(module.length() + 1);
return getServletFromModule(moduleMapping, module, modulePath);
}
}
throw new RuntimeException("No module configuration found for path "
+ path);
}
/** Obtains the servlet configuration from a url path fragment.
*
* @param moduleMapping The uri to servlet mapping.
*
* @param moduleName The name of the module.
*
* @param path The url fragment that follows the name of the module.
*
* @return Returs all the data needed to forward the request to the correct
* servlet. It never returns null.
*/
private ServletData getServletFromModule(
final Map<String, ServletAndParameters> moduleMapping,
final String moduleName, final String path) {
/*
requestURL = http://localhost:8080/katari-i/welcome.do
requestURI = /katari-i/welcome.do
contextPath = /katari-i
servletPath = /welcome.do
pathInfo = null
*/
Validate.notNull(path, "The path cannot be null");
for (Map.Entry<String, ServletAndParameters> entry
: moduleMapping.entrySet()) {
String uri = entry.getKey();
if (log.isDebugEnabled()) {
log.debug("Verifying if path " + path + " matches " + uri);
}
Pattern pattern = Pattern.compile(uri);
Matcher matcher = pattern.matcher(path);
if (matcher.lookingAt()) {
if (log.isDebugEnabled()) {
log.debug("Matched " + uri);
}
ServletAndParameters servletAndParameters = entry.getValue();
HttpServlet servlet = servletAndParameters.getServlet();
String servletPath = matcher.group();
return new ServletData(servlet, moduleName, servletPath);
}
}
throw new RuntimeException("No servlet found for path " + path);
}
/** Checks if the request correponds to a servlet include.
*
* @param request The request to check for include. It cannot be null.
*
* @return true if it is an include, false otherwise.
*/
private boolean isIncluded(final HttpServletRequest request) {
Validate.notNull(request, "The request cannot be null");
return request.getAttribute("javax.servlet.include.request_uri") != null;
}
/** This class contains the data necessary to forward a request to a servlet.
*/
public static final class ServletData {
/** The servlet.
*
* It is never null.
*/
private HttpServlet servlet;
/** The module name.
*
* It is never null.
*/
private String moduleName;
/** The path where this servlet is mapped.
*
* It is never null.
*/
private String servletPath;
/** Creates a servlet data.
*
* @param theServlet The servlet that handles the requests. It cannot
* be null.
*
* @param theModuleName The name of the module containing this servlet.
* It cannot be null.
*
* @param theServletPath The url path where this servlet is mapped. It
* cannot be null.
*/
public ServletData(final HttpServlet theServlet, final String
theModuleName, final String theServletPath) {
Validate.notNull(theServlet, "The servlet cannot be null");
Validate.notNull(theModuleName, "The module name cannot be null");
Validate.notNull(theServletPath, "The servlet path cannot be null");
servlet = theServlet;
moduleName = theModuleName;
servletPath = theServletPath;
}
/** Returns the servlet.
*
* @return the servlet. It never returns null.
*/
public HttpServlet getServlet() {
return servlet;
}
/** Returns the module name.
*
* @return the module name. It never returns null.
*/
public String getModuleName() {
return moduleName;
}
/** Returns the servlet path.
*
* @return the servlet path. It never returns null.
*/
public String getServletPath() {
return servletPath;
}
}
}