/* * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package com.arsdigita.dispatcher; import com.arsdigita.developersupport.DeveloperSupport; import com.arsdigita.kernel.Kernel; import com.arsdigita.util.StringUtils; import com.arsdigita.util.UncheckedWrapperException; import com.arsdigita.web.RedirectSignal; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.List; import java.util.Vector; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.apache.log4j.Logger; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * Contains functions common to all entry-point dispatcher servlets in the core. * * Any dispatcher that is the first in its chain to handle an HTTP request * must also be a servlet and should extend this class. * *
You do not need to extend this class unless your
* dispatcher is also a servlet and is mounted in web.xml. In any
* given ACS installation, you generally only have one servlet that is
* mounted through web.xml, and that is usually the
* com.arsdigita.sitenode.SiteNodeDispatcher, mapped to URL
* "/".
*
*
When a request comes in: * *
dispatch
* method.
*
* this.
* @param rl the RequestListener to add to the
* listener list
*/
public static void addRequestListener(RequestListener rl) {
s_listenerList.add(rl);
}
/**
* A placeholder method for performing user authentication during
* request processing. Subclasses should override this method.
*
* @param req the current servlet request object
* @param req the current servlet response object
* @param req the current request context
* @return the updated request context (which may be the same as the context
* context parameter).
*
* @throws com.arsdigita.dispatcher.RedirectException if the dispatcher
* should redirect the client to the page contained in the exception
**/
protected abstract RequestContext authenticateUser(HttpServletRequest req,
HttpServletResponse resp,
RequestContext ctx)
throws RedirectException;
/**
* Called directly by the servlet container when this servlet is invoked
* from a URL request. First tries to dispatch the URL to a
* concrete file on disk, if there is a matching file. Otherwise,
* sets up an initial RequestContext, tries to
* identify the user/session, parses form variables, and wraps the
* request object to handle multipart forms if necessary. Calls
* the dispatch method as declared in implementing
* subclasses.
* @param req the servlet request
* @param resp the servlet response
* @throws javax.servlet.ServletException re-thrown when
* dispatch throws an exception
* @throws java.io.IOException re-thrown when dispatch
* throws an IOException
*/
public void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (s_log.isDebugEnabled()) {
s_log.debug("\n*** *** *** *** *** ***\n"
+ "Servicing request for URL '" + req.getRequestURI()
+ "'\n" + "*** *** *** *** *** ***");
}
boolean reentrant = true;
RequestContext reqCtx =
DispatcherHelper.getRequestContext(req);
boolean finishedNormal = false;
// there are two types of re-entrancy we need to consider:
// * forwarded requests specified by the application,
// where the forwarded request is picked up by a subclass
// of BDS. (e.g., SND forwards /foo/bar to
// /packages/foo/www/bar.jsp)
// * a secondary request, forwarded by the CONTAINER in response
// to an exception thrown from service(), after the first request
// completes
//
// in the FIRST case, we need to guard against running
// the start/end listeners again. in the SECOND case,
// we need to treat this like a new request so that
// we open a transaction, etc. for serving the error page.
// wrap entire rest of method in try-catch block. that way if
// some method call throws ServletException or IOException,
// implicitly exiting the service method, we'll still be able
// to free up the database connection in a finally.
// STEP #1: if no extension, treat as directory;
// make sure we have a trailing slash. and redirect
//otherwise.
DispatcherHelper.setRequest(req);
if (trailingSlashRedirect(req, resp)) {
// note, this is OUTSIDE of try/catch/finally. No
// listeners of any kind are run!
return;
}
// STEP #2: try to serve concrete static file, if one exists.
// (defer serving concrete JSPs until after listeners run)
int concreteFileType = concreteFileType(req);
if (concreteFileType == STATIC_FILE) {
s_log.debug("Setting world cache headers on static file");
DispatcherHelper.cacheForWorld(resp);
DispatcherHelper.forwardRequestByName("default", req, resp,
getServletContext());
return;
}
try {
if (req.getAttribute(REENTRANCE_ATTRIBUTE) == null) {
reentrant = false;
waitForPreviousRequestToFinish(req);
// need an identifier for this particular request
String requestId =
Thread.currentThread().getName() + "|" + System.
currentTimeMillis();
req.setAttribute(REENTRANCE_ATTRIBUTE, requestId);
s_activeList.add(requestId);
DeveloperSupport.startStage("BaseDispatcherServlet.service()");
try {
// first time through:
// do all actions that must be done initially on hit
StartRequestRecord srr = startRequest(req, resp);
reqCtx = srr.m_reqCtx;
req = srr.m_req;
s_log.debug("After startRequest the request is now " + req);
} catch (RedirectException re) {
final String url = re.getRedirectURL();
resp.sendRedirect(resp.encodeRedirectURL(url));
return;
}
} else {
req = DispatcherHelper.maybeWrapRequest(req);
// if we're handling a secondary request for an
// error, but we haven't run the finally ... block
// on the primary request yet (this happens when
// sendError is called explicitly, as opposed to when
// the container calls sendError(500...) in response
// to an exception rethrown here) we DON'T run the
// request listeners. BUT we need to clear
// the request context.
if (req.getAttribute(ERROR_REQUEST_ATTRIBUTE) != null
|| req.getAttribute(JSP_EXCEPTION_ATTRIBUTE) != null) {
// reset URL boookeeping but don't wipe out
// whole object since it might actually be a
// KernelRequestContext with user / session info
if (reqCtx instanceof InitialRequestContext) {
((InitialRequestContext) reqCtx).
initializeURLFromRequest(req, true);
}
}
}
// finally, call dispatch
finishedNormal = false;
if (concreteFileType == JSP_FILE) {
// STEP #3: dispatch to a concrete JSP if we have a matching
// one
DispatcherHelper.forwardRequestByName("jsp", req, resp);
} else {
// STEP #4: if no concrete file exists, dispatch to
// implementing class
dispatch(req, resp, reqCtx);
}
// if JSP already dispatched to error page, no exception
// will be thrown. have to check for attribute manually.
if (req.getAttribute(JSP_EXCEPTION_ATTRIBUTE) == null) {
finishedNormal = true;
}
} catch (AbortRequestSignal ars) {
// treat this as a normal end of request and
// try to commit
finishedNormal = true;
} catch (IOException ioe) {
s_log.error("error in BaseDispatcherServlet", ioe);
throw ioe;
} catch (ServletException se) {
// SDM #140226, improved handling of
// ServletException.getRootCause()
Throwable t = se;
Throwable rootError;
do {
rootError = t;
t = ((ServletException) t).getRootCause();
} while (t instanceof ServletException);
if (t != null) {
rootError = t;
}
// handle this in case AbortRequestSignal got wrapped
// accidentally--e.g., inside a JSP.
if (rootError != null
&& (rootError instanceof AbortRequestSignal)) {
finishedNormal = true;
} else if (rootError != null
&& (rootError instanceof RedirectSignal)) {
s_log.debug("rethrowing RedirectSignal", rootError);
throw (RedirectSignal) rootError;
} else {
s_log.error("error in BaseDispatcherServlet", rootError);
throw new ServletException(rootError);
}
} catch (RuntimeException re) {
s_log.error("error in BaseDispatcherServlet", re);
throw re;
} catch (Error error) {
s_log.error("error in BaseDispatcherServlet", error);
throw error;
} finally {
if (!reentrant) {
DeveloperSupport.endStage("BaseDispatcherServlet.service()");
// run the request listener events
fireFinishedListener(
new RequestEvent(req, resp, reqCtx, false,
finishedNormal));
// at this point, clear the attribute so
// a secondary request will work
// and remove the request from the list of currently-active
// requests
Object requestId = req.getAttribute(REENTRANCE_ATTRIBUTE);
synchronized (s_activeList) {
s_activeList.remove(requestId);
s_activeList.notifyAll();
}
req.removeAttribute(REENTRANCE_ATTRIBUTE);
}
}
}
/**
* Processes a request when it is first handled by the servlet. This
* method runs exactly once for each request, even if the request is
* reentrant.
*
* @return a tuple containing the updated request context and the request
*
* @throws com.arsdigita.dispatcher.RedirectException if the dispatcher
* should redirect the client to the page contained in the exception
**/
private StartRequestRecord startRequest(HttpServletRequest req,
HttpServletResponse resp)
throws RedirectException, IOException, ServletException {
// turn multipart request into wrapped request
// to make up for servlet 2.2 brokenness
req = DispatcherHelper.maybeWrapRequest(req);
RequestContext reqCtx =
new InitialRequestContext(req, getServletContext());
// run the request listener events
fireStartListener(new RequestEvent(req, resp, reqCtx, true));
// Authenticate user AFTER request listeners because authentication
// may need to use the database connection (opened by a listener).
// Allow subclass to update request context with user info.
reqCtx = authenticateUser(req, resp, reqCtx);
// save the request context in the request
DispatcherHelper.setRequestContext(req, reqCtx);
return new StartRequestRecord(reqCtx, req);
}
/**
* Fires all finished listeners. Collects and logs errors to ensure
* that all finished listeners run.
*
* @param evt the current RequestEvent to broadcast to all event
* listeners
*/
protected void fireFinishedListener(RequestEvent evt) {
for (int i = 0; i < s_listenerList.size(); i++) {
try {
((RequestListener) s_listenerList.get(i)).requestFinished(evt);
} catch (Exception e) {
s_log.error("Error running request finished listener " + s_listenerList.
get(i) + " (#" + i + ")", e);
}
}
}
/**
* Fires all start listeners. Does not collect and log errors.
* Instead, a runtime failure in a start listener will inhibit further
* servicing of the request.
* @param evt the current RequestEvent to broadcast to all event
* listeners
*/
protected void fireStartListener(RequestEvent evt) {
for (int i = 0; i < s_listenerList.size(); i++) {
((RequestListener) s_listenerList.get(i)).requestStarted(evt);
}
}
/**
* Kludge for returning a typed 2-tuple.
*/
private class StartRequestRecord {
RequestContext m_reqCtx;
HttpServletRequest m_req;
public StartRequestRecord(RequestContext rc, HttpServletRequest req) {
m_reqCtx = rc;
m_req = req;
}
}
private void waitForPreviousRequestToFinish(HttpServletRequest req) {
// handle concurrence -- serialize requests from the same
// user agent, so that you can't follow a link/redirect from
// a request until the request's transaction has committed
// get identifier from previous request, if there is any
HttpSession sess = req.getSession(false);
if (sess != null) {
Object sema = sess.getAttribute(REDIRECT_SEMAPHORE);
if (sema != null) {
while (s_activeList.indexOf(sema) != -1) {
try {
synchronized (s_activeList) {
s_activeList.wait();
}
} catch (InterruptedException ie) {
}
}
sess.removeAttribute(REDIRECT_SEMAPHORE);
}
}
}
/**
* helper method: if the current request URL points to a concrete
* file under the webapp root, returns STATIC_FILE or JSP_FILE
* indicating the type of file. returns NOT_FOUND if no corresponding
* concrete file exists.
*
* If the concrete file is a directory, then we require * that the directory have a welcome file like index.*; this * prevents us from serving directory listings. For * directories we return STATIC_FILE if there is a welcome file, * otherwise return NOT_FOUND. * * @return STATIC_FILE if the current request points to a concrete * static file (non-JSP) or a directory that has a welcome file. * returns JSP_FILE if it corresponds to a dynamic JSP file. * returns NOT_FOUND otherwise. */ private int concreteFileType(HttpServletRequest req) throws ServletException, IOException { String path = DispatcherHelper.getCurrentResourcePath(req); ServletContext sctx = this.getServletContext(); File realFile = new File(sctx.getRealPath(path)); if (realFile.exists() && (!realFile.isDirectory() || hasWelcomeFile( realFile))) { // yup. Go there, bypass the site map. // we have a concrete file so no forwarding to // rewrite the request URL is necessary. if (realFile.getName().endsWith(".jsp")) { return JSP_FILE; } else { return STATIC_FILE; } } else { return NOT_FOUND; } } /** * returns true if dir is a directory and has a welcome file * like index.*. * @pre dir.isDirectory() */ private boolean hasWelcomeFile(File dir) { if (!dir.isDirectory()) { throw new IllegalArgumentException("dir must be a directory"); } String[] files = dir.list(); for (int i = 0; i < files.length; i++) { if (m_welcomeFiles.indexOf(files[i]) >= 0) { return true; } } return false; } private boolean trailingSlashRedirect(HttpServletRequest req, HttpServletResponse resp) throws IOException { String path = DispatcherHelper.getCurrentResourcePath(req); // first, see if we have an extension if (path.lastIndexOf(".") <= path.lastIndexOf("/")) { // maybe no extension. check if there's a trailing // slash already. if (!path.endsWith("/")) { // no trailing slash String targetURL = req.getContextPath() + path + "/"; String query = req.getQueryString(); if (query != null && query.length() > 0) { targetURL += "?" + query; } resp.sendRedirect(resp.encodeRedirectURL(targetURL)); return true; } } return false; } /** * SAX content handler class to pick welcome-file-list out of * web.xml */ private class WebXMLReader extends DefaultHandler { StringBuffer m_buffer = new StringBuffer(); public InputSource resolveEntity(String publicId, String systemId) throws SAXException { // we don't want to read the web.xml dtd if (WEB_XML_22_PUBLIC_ID.equals(publicId) || WEB_XML_23_PUBLIC_ID.equals(publicId)) { StringReader reader = new StringReader(" "); return new InputSource(reader); } else { try { return super.resolveEntity(publicId, systemId); } catch (Exception e) { if (e instanceof SAXException) { throw (SAXException) e; } else { throw new UncheckedWrapperException("Resolve Error", e); } } } } public void characters(char[] ch, int start, int len) { for (int i = 0; i < len; i++) { m_buffer.append(ch[start + i]); } } public void endElement(String uri, String localName, String qname) { if (qname.equals("welcome-file-list")) { String[] welcomeFiles = StringUtils.split(m_buffer.toString(), ','); for (int i = 0; i < welcomeFiles.length; i++) { m_welcomeFiles.add(welcomeFiles[i].trim()); } } m_buffer = new StringBuffer(); } } }