/* * This file is part of the Wayback archival access software * (http://archive-access.sourceforge.net/projects/wayback/). * * Licensed to the Internet Archive (IA) by one or more individual * contributors. * * The IA 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.archive.wayback.util.webapp; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.logging.Logger; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.archive.wayback.core.UIResults; /** * This class maintains a mapping of RequestHandlers and ShutDownListeners, to * allow (somewhat) efficient mapping and delegation of incoming requests to * the appropriate RequestHandler. * * This class uses PortMapper to delegate some of the responsibility of mapping * requests received on a particular port, and also allows configuration of a * global PRE RequestHandler, which gets first dibs on EVERY incoming request, * as well as a global POST RequestHandler, which may attempt to handle any * incoming request not handled by the normal RequestHandler mapping. * * @author brad * */ public class RequestMapper { private static final Logger LOGGER = Logger.getLogger( RequestMapper.class.getName()); private ArrayList<ShutdownListener> shutdownListeners = null; private HashMap<Integer,PortMapper> portMap = null; private RequestHandler globalPreRequestHandler = null; private RequestHandler globalPostRequestHandler = null; /** * The name of an attribute for storing the prefix of URL * path corresponding to the {@link RequestHandler} processing * the request. */ public static final String REQUEST_CONTEXT_PREFIX = "webapp-request-context-path-prefix"; /** * Bean name used to register the special global PRE RequestHandler. */ public final static String GLOBAL_PRE_REQUEST_HANDLER = "-"; /** * Bean name used to register the special global POST RequestHandler. */ public final static String GLOBAL_POST_REQUEST_HANDLER = "+"; /** * Construct a RequestMapper, for the given RequestHandler objects, on the * specified ServletContext. This method will call setServletContext() on * each RequestMapper, followed immediately by registerPortListener() * * @param requestHandlers Collection of RequestHandlers which handle * requests * @param servletContext the webapp ServletContext where this RequestMapper * is configured. */ public RequestMapper(Collection<RequestHandler> requestHandlers, ServletContext servletContext) { portMap = new HashMap<Integer, PortMapper>(); shutdownListeners = new ArrayList<ShutdownListener>(); Iterator<RequestHandler> itr = requestHandlers.iterator(); LOGGER.info("Registering handlers."); while(itr.hasNext()) { RequestHandler requestHandler = itr.next(); requestHandler.setServletContext(servletContext); requestHandler.registerPortListener(this); } LOGGER.info("Registering handlers complete."); } /** * Request the shutdownListener object to be notified of ServletContext * shutdown. * @param shutdownListener the object which needs to have shutdown() called * when the ServletContext is destroyed. */ public void addShutdownListener(ShutdownListener shutdownListener) { shutdownListeners.add(shutdownListener); } /** * Configure the specified RequestHandler to handle ALL incoming requests * before any other normal mapping. * @param requestHandler the global PRE RequestHandler */ public void addGlobalPreRequestHandler(RequestHandler requestHandler) { globalPreRequestHandler = requestHandler; } /** * Configure the specified RequestHandler to handle ALL incoming requests * after all other normal mapping has been attempted * @param requestHandler the global POST RequestHandler */ public void addGlobalPostRequestHandler(RequestHandler requestHandler) { globalPostRequestHandler = requestHandler; } /** * Register the RequestHandler to accept requests on the given port, for the * specified host and path. * @param port the integer port on which the RequestHandler gets requests. * @param host the String Host which the RequestHandler matches, or null, if * the RequestHandler should match ALL hosts. * @param path the String path which the RequestHandler matches, or null, if * the RequestHandler should match ALL paths. * @param requestHandler the RequestHandler to register. */ public void addRequestHandler(int port, String host, String path, RequestHandler requestHandler) { Integer portInt = Integer.valueOf(port); PortMapper portMapper = portMap.get(portInt); if (portMapper == null) { portMapper = new PortMapper(portInt); portMap.put(portInt, portMapper); } portMapper.addRequestHandler(host, path, requestHandler); LOGGER.info("Registered " + port + "/" + (host == null ? "*" : host) + "/" + (path == null ? "*" : path) + " --> " + requestHandler); } public RequestHandlerContext mapRequest(HttpServletRequest request) { RequestHandlerContext handlerContext = null; int port = request.getLocalPort(); Integer portInt = Integer.valueOf(port); PortMapper portMapper = portMap.get(portInt); if (portMapper != null) { handlerContext = portMapper.getRequestHandlerContext(request); } else { LOGGER.warning("No PortMapper for port " + port); } return handlerContext; } /** * Map the incoming request to the appropriate RequestHandler, including * the PRE and POST RequestHandlers, if configured. * @param request the incoming HttpServletRequest * @param response the HttpServletResponse to return data to the client * @return true if a response was returned to the client. * @throws ServletException for usual reasons. * @throws IOException for usual reasons. */ public boolean handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { boolean handled = false; // Internally UIResults.forward(), don't handle here if (request.getAttribute(UIResults.FERRET_NAME) != null) { return false; } if (globalPreRequestHandler != null) { handled = globalPreRequestHandler.handleRequest(request, response); } if (handled == false) { RequestHandlerContext handlerContext = mapRequest(request); if (handlerContext != null) { RequestHandler requestHandler = handlerContext.getRequestHandler(); // need to add trailing "/" iff prefix is not "/": String pathPrefix = handlerContext.getPathPrefix(); if (!pathPrefix.equals("/")) { pathPrefix += "/"; } request.setAttribute(REQUEST_CONTEXT_PREFIX,pathPrefix); handled = requestHandler.handleRequest(request, response); } } if (handled == false) { if(globalPostRequestHandler != null) { handled = globalPostRequestHandler.handleRequest(request, response); } } return handled; } /** * notify all registered ShutdownListener objects that the ServletContext is * being destroyed. */ public void shutdown() { for (ShutdownListener shutdownListener : shutdownListeners) { try { shutdownListener.shutdown(); } catch(Exception e) { LOGGER.severe("failed shutdown"+e.getMessage()); } } } /** * Extract the request path prefix, as computed at RequestHandler mapping, * from the HttpServletRequest object. * * @param request HttpServlet request object being handled * @return the portion of the original request path which indicated the * RequestHandler, including the trailing '/'. */ public static String getRequestPathPrefix(HttpServletRequest request) { return (String) request.getAttribute(REQUEST_CONTEXT_PREFIX); } /** * @param request HttpServlet request object being handled * @return the portion of the incoming path within the RequestHandler * handling the request, not including a leading "/", and not including * query arguments. */ public static String getRequestContextPath(HttpServletRequest request) { String prefix = (String) request.getAttribute(REQUEST_CONTEXT_PREFIX); String requestUrl = request.getRequestURI(); if (prefix == null) { return requestUrl; } if (requestUrl.startsWith(prefix)) { return requestUrl.substring(prefix.length()); } return requestUrl; } /** * @param request HttpServlet request object being handled * @return the portion of the incoming path within the RequestHandler * handling the request, not including a leading "/", including query * arguments. */ public static String getRequestContextPathQuery(HttpServletRequest request) { String prefix = (String)request.getAttribute(REQUEST_CONTEXT_PREFIX); // HttpServletRequest.getRequestURI() returns path portion of request URL // (does not include query part), *undecoded*. StringBuilder sb = new StringBuilder(request.getRequestURI()); String requestUrl = null; String query = request.getQueryString(); if (query != null) { requestUrl = sb.append("?").append(query).toString(); } else { requestUrl = sb.toString(); } if (prefix == null) { return requestUrl; } if (requestUrl.startsWith(prefix)) { return requestUrl.substring(prefix.length()); } // Fix for access point with missing end slash else if (prefix.endsWith("/") && (requestUrl + "/").equals(prefix)) { return ""; } return requestUrl; } }