View Javadoc
1   /*
2    *  This file is part of the Wayback archival access software
3    *   (http://archive-access.sourceforge.net/projects/wayback/).
4    *
5    *  Licensed to the Internet Archive (IA) by one or more individual 
6    *  contributors. 
7    *
8    *  The IA licenses this file to You under the Apache License, Version 2.0
9    *  (the "License"); you may not use this file except in compliance with
10   *  the License.  You may obtain a copy of the License at
11   *
12   *      http://www.apache.org/licenses/LICENSE-2.0
13   *
14   *  Unless required by applicable law or agreed to in writing, software
15   *  distributed under the License is distributed on an "AS IS" BASIS,
16   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   *  See the License for the specific language governing permissions and
18   *  limitations under the License.
19   */
20  package org.archive.wayback.util.webapp;
21  
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.logging.Logger;
28  
29  import javax.servlet.ServletContext;
30  import javax.servlet.ServletException;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  
34  import org.archive.wayback.core.UIResults;
35  
36  /**
37   * This class maintains a mapping of RequestHandlers and ShutDownListeners, to
38   * allow (somewhat) efficient mapping and delegation of incoming requests to
39   * the appropriate RequestHandler.
40   * 
41   * This class uses PortMapper to delegate some of the responsibility of mapping
42   * requests received on a particular port, and also allows configuration of a
43   * global PRE RequestHandler, which gets first dibs on EVERY incoming request,
44   * as well as a global POST RequestHandler, which may attempt to handle any
45   * incoming request not handled by the normal RequestHandler mapping.
46   * 
47   * @author brad
48   *
49   */
50  public class RequestMapper {
51  
52  	private static final Logger LOGGER = Logger.getLogger(
53  			RequestMapper.class.getName());
54  	
55  	private ArrayList<ShutdownListener> shutdownListeners = null;
56  	
57  	private HashMap<Integer,PortMapper> portMap = null;
58  	private RequestHandler globalPreRequestHandler = null;
59  	private RequestHandler globalPostRequestHandler = null;
60  	
61  	/**
62  	 * The name of an attribute for storing the prefix of URL
63  	 * path corresponding to the {@link RequestHandler} processing
64  	 * the request.
65  	 */
66  	public static final String REQUEST_CONTEXT_PREFIX =
67  		"webapp-request-context-path-prefix";
68  	
69  	/**
70  	 * Bean name used to register the special global PRE RequestHandler. 
71  	 */
72  	public final static String GLOBAL_PRE_REQUEST_HANDLER = "-";
73  	/**
74  	 * Bean name used to register the special global POST RequestHandler. 
75  	 */
76  	public final static String GLOBAL_POST_REQUEST_HANDLER = "+";
77  
78  	/**
79  	 * Construct a RequestMapper, for the given RequestHandler objects, on the
80  	 * specified ServletContext. This method will call setServletContext() on
81  	 * each RequestMapper, followed immediately by registerPortListener()
82  	 * 
83  	 * @param requestHandlers Collection of RequestHandlers which handle 
84  	 * requests
85  	 * @param servletContext the webapp ServletContext where this RequestMapper
86  	 * is configured.
87  	 */
88  	public RequestMapper(Collection<RequestHandler> requestHandlers,
89  			ServletContext servletContext) {
90  		portMap = new HashMap<Integer, PortMapper>();
91  		shutdownListeners = new ArrayList<ShutdownListener>();
92  		Iterator<RequestHandler> itr = requestHandlers.iterator();
93  		LOGGER.info("Registering handlers.");
94  		while(itr.hasNext()) {
95  			RequestHandler requestHandler = itr.next();
96  			requestHandler.setServletContext(servletContext);
97  			requestHandler.registerPortListener(this);
98  		}
99  		LOGGER.info("Registering handlers complete.");
100 	}
101 
102 	/**
103 	 * Request the shutdownListener object to be notified of ServletContext
104 	 * shutdown.
105 	 * @param shutdownListener the object which needs to have shutdown() called
106 	 * when the ServletContext is destroyed.
107 	 */
108 	public void addShutdownListener(ShutdownListener shutdownListener) {
109 		shutdownListeners.add(shutdownListener);
110 	}
111 	/**
112 	 * Configure the specified RequestHandler to handle ALL incoming requests
113 	 * before any other normal mapping.
114 	 * @param requestHandler the global PRE RequestHandler
115 	 */
116 	public void addGlobalPreRequestHandler(RequestHandler requestHandler) {
117 		globalPreRequestHandler = requestHandler;
118 	}
119 	/**
120 	 * Configure the specified RequestHandler to handle ALL incoming requests
121 	 * after all other normal mapping has been attempted
122 	 * @param requestHandler the global POST RequestHandler
123 	 */
124 	public void addGlobalPostRequestHandler(RequestHandler requestHandler) {
125 		globalPostRequestHandler = requestHandler;
126 	}
127 	/**
128 	 * Register the RequestHandler to accept requests on the given port, for the
129 	 * specified host and path.
130 	 * @param port the integer port on which the RequestHandler gets requests.
131 	 * @param host the String Host which the RequestHandler matches, or null, if
132 	 * the RequestHandler should match ALL hosts.
133 	 * @param path the String path which the RequestHandler matches, or null, if
134 	 * the RequestHandler should match ALL paths.
135 	 * @param requestHandler the RequestHandler to register.
136 	 */
137 	public void addRequestHandler(int port, String host, String path, 
138 			RequestHandler requestHandler) {
139 		Integer portInt = Integer.valueOf(port);
140 		PortMapper portMapper = portMap.get(portInt);
141 		if (portMapper == null) {
142 			portMapper = new PortMapper(portInt);
143 			portMap.put(portInt, portMapper);
144 		}
145 		portMapper.addRequestHandler(host, path, requestHandler);
146 		LOGGER.info("Registered " + port + "/" +
147 				(host == null ? "*" : host) + "/" +
148 				(path == null ? "*" : path) + " --> " +
149 				requestHandler);
150 	}
151 
152 	public RequestHandlerContext mapRequest(HttpServletRequest request) {
153 		RequestHandlerContext handlerContext = null;
154 		
155 		int port = request.getLocalPort();
156 		Integer portInt = Integer.valueOf(port);
157 		PortMapper portMapper = portMap.get(portInt);
158 		if (portMapper != null) {
159 			handlerContext = portMapper.getRequestHandlerContext(request);
160 		} else {
161 			LOGGER.warning("No PortMapper for port " + port);
162 		}
163 		return handlerContext;
164 	}
165 	
166 	/**
167 	 * Map the incoming request to the appropriate RequestHandler, including
168 	 * the PRE and POST RequestHandlers, if configured.
169 	 * @param request the incoming HttpServletRequest
170 	 * @param response the HttpServletResponse to return data to the client
171 	 * @return true if a response was returned to the client.
172 	 * @throws ServletException for usual reasons.
173 	 * @throws IOException for usual reasons.
174 	 */
175 	public boolean handleRequest(HttpServletRequest request,
176 			HttpServletResponse response) throws IOException, ServletException {
177 		boolean handled = false;
178 		
179 		// Internally UIResults.forward(), don't handle here
180 		if (request.getAttribute(UIResults.FERRET_NAME) != null) {
181 			return false;
182 		}
183 		
184 		if (globalPreRequestHandler != null) {
185 			handled = globalPreRequestHandler.handleRequest(request, response);
186 		}
187 		if (handled == false) {
188 			RequestHandlerContext handlerContext = mapRequest(request);
189 			if (handlerContext != null) {
190 				RequestHandler requestHandler = 
191 					handlerContext.getRequestHandler();
192 				// need to add trailing "/" iff prefix is not "/":
193 				String pathPrefix = handlerContext.getPathPrefix();
194 				if (!pathPrefix.equals("/")) {
195 					pathPrefix += "/";
196 				}
197 				request.setAttribute(REQUEST_CONTEXT_PREFIX,pathPrefix); 
198 				handled = requestHandler.handleRequest(request, response);
199 			}
200 		}
201 		if (handled == false) {
202 			if(globalPostRequestHandler != null) {
203 				handled = globalPostRequestHandler.handleRequest(request,
204 						response);
205 			}
206 		}
207 			
208 		return handled;
209 	}
210 
211 	/**
212 	 * notify all registered ShutdownListener objects that the ServletContext is
213 	 * being destroyed.
214 	 */
215 	public void shutdown() {
216 		for (ShutdownListener shutdownListener : shutdownListeners) {
217 			try {
218 				shutdownListener.shutdown();
219 			} catch(Exception e) {
220 				LOGGER.severe("failed shutdown"+e.getMessage());
221 			}
222 		}
223 	}
224 	
225 	/**
226 	 * Extract the request path prefix, as computed at RequestHandler mapping,
227 	 * from the HttpServletRequest object.
228 	 * 
229 	 * @param request HttpServlet request object being handled
230 	 * @return the portion of the original request path which indicated the 
231 	 * RequestHandler, including the trailing '/'.
232 	 */
233 	public static String getRequestPathPrefix(HttpServletRequest request) {
234 		return (String) request.getAttribute(REQUEST_CONTEXT_PREFIX);
235 	}
236 
237 	/**
238 	 * @param request HttpServlet request object being handled
239 	 * @return the portion of the incoming path within the RequestHandler
240 	 * handling the request, not including a leading "/", and not including 
241 	 * query arguments.
242 	 */
243 	public static String getRequestContextPath(HttpServletRequest request) {
244 		String prefix = (String) request.getAttribute(REQUEST_CONTEXT_PREFIX);
245 		String requestUrl = request.getRequestURI();
246 		if (prefix == null) {
247 			return requestUrl;
248 		}
249 		if (requestUrl.startsWith(prefix)) {
250 			return requestUrl.substring(prefix.length());
251 		}
252 		return requestUrl;
253 	}
254 
255 	/**
256 	 * @param request HttpServlet request object being handled
257 	 * @return the portion of the incoming path within the RequestHandler
258 	 * handling the request, not including a leading "/", including query 
259 	 * arguments.
260 	 */
261 	public static String getRequestContextPathQuery(HttpServletRequest request) {
262 		String prefix = (String)request.getAttribute(REQUEST_CONTEXT_PREFIX);
263 		// HttpServletRequest.getRequestURI() returns path portion of request URL
264 		// (does not include query part), *undecoded*.
265 		StringBuilder sb = new StringBuilder(request.getRequestURI());
266 		String requestUrl = null;
267 		String query = request.getQueryString();
268 		if (query != null) {
269 			requestUrl = sb.append("?").append(query).toString();
270 		} else {
271 			requestUrl = sb.toString();
272 		}
273 		if (prefix == null) {
274 			return requestUrl;
275 		}
276 		if (requestUrl.startsWith(prefix)) {
277 			return requestUrl.substring(prefix.length());
278 		} 
279 		// Fix for access point with missing end slash
280 		else if (prefix.endsWith("/") && (requestUrl + "/").equals(prefix)) {
281 			return "";
282 		}
283 		return requestUrl;
284 	}
285 }