View Javadoc

1   /*--------------------------------------------------------------------------
2    *  Copyright 2007 utgenome.org
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   *--------------------------------------------------------------------------*/
16  //--------------------------------------
17  // utgb-core Project
18  //
19  // RequestDispatcher.java
20  // Since: Oct 4, 2007
21  //
22  // $URL$ 
23  // $Author$
24  //--------------------------------------
25  package org.utgenome.gwt.utgb.server;
26  
27  import java.io.BufferedInputStream;
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.IOException;
31  import java.io.OutputStream;
32  import java.text.DateFormat;
33  import java.text.SimpleDateFormat;
34  import java.util.Calendar;
35  import java.util.Date;
36  import java.util.Enumeration;
37  import java.util.HashMap;
38  import java.util.LinkedList;
39  import java.util.Locale;
40  import java.util.Properties;
41  import java.util.regex.Matcher;
42  import java.util.regex.Pattern;
43  
44  import javax.servlet.Filter;
45  import javax.servlet.FilterChain;
46  import javax.servlet.FilterConfig;
47  import javax.servlet.ServletException;
48  import javax.servlet.ServletRequest;
49  import javax.servlet.ServletResponse;
50  import javax.servlet.http.HttpServletRequest;
51  import javax.servlet.http.HttpServletResponse;
52  
53  import org.utgenome.UTGBException;
54  import org.xerial.core.XerialException;
55  import org.xerial.lens.Lens;
56  import org.xerial.util.bean.BeanUtil;
57  import org.xerial.util.log.Logger;
58  
59  /**
60   * {@link RequestDispatcher} dispatches HTTP GET/POST request to appropriate action handlers.
61   * 
62   * see also http://trac.utgenome.org/project/UTGB/wiki/UTGBCore/RequestDispatcher
63   * 
64   * <h1>UTGB Request Dispatcher Mechanism</h1>
65   * 
66   * <h2>web.xml setting</h2>
67   * 
68   * Add the following description into your web.xml.
69   * 
70   * <li>The lines param-name=base-package, param-value=(application base) specifies the location (base package name)
71   * where the RequestDispatcher? searches recursively for RequestHandler? implementations.
72   * 
73   * <li>With the following setting, an HTTP request, e.g., http://localhost:8989/hello.action, is mapped to the request
74   * handler org.utgenome.gwt.utgb.server.app.Hello class. The upper letters are converted into the lower letters when
75   * mapping the request, e.g., Hello action can be accecced via hello.action URL.
76   * 
77   * <li>Another example using hierarchies of actions: http://localhost:8989/admin/login.action is mapped to
78   * org.utgenome.gwt.utgb.server.Login class.
79   * 
80   * <pre>
81   *  &lt;servlet&gt;
82   *  &lt;servlet-name&gt;dispatcher&lt;/servlet-name&gt;
83   *  &lt;servlet-class&gt;org.utgenome.gwt.utgb.server.RequestDispatcher&lt;/servlet-class&gt;
84   *  &lt;init-param&gt;
85   *  &lt;param-name&gt;base-package&lt;/param-name&gt;
86   *  &lt;param-value&gt;(your web application request handler base package. e.g. org.utgenome.gwt.utgb.server.app)&lt;/param-value&gt;
87   *  &lt;/init-param&gt;
88   *  &lt;/servlet&gt;
89   * 
90   *  &lt;servlet-mapping&gt;
91   *  &lt;servlet-name&gt;dispatcher&lt;/servlet-name&gt;
92   *  &lt;url-pattern&gt;*.action&lt;/url-pattern&gt;
93   *  &lt;/servlet-mapping&gt;
94   * </pre>
95   * 
96   * <h2>Automatic Data Binding to Request Handler</h2>
97   * 
98   * Hello.java class has serveral parameter values name and year:
99   * 
100  * <code>
101  <pre>
102  * public class Hello implements RequestHandler {
103  * 	private String name = &quot;&quot;;
104  * 	private int year = 2000;
105  * 
106  * 	public void handle(HttpServletRequest request, HttpServletResponse response) {
107  * 		PrintWriter out = response.getWriter().println(&quot;Hello &quot; + name + &quot;(&quot; + year + &quot;)&quot;);
108  * 	}
109  * 
110  * 	public void setName(String name) {
111  * 		this.name = name;
112  * 	}
113  * 
114  * 	public void setYear(int year) {
115  * 		this.year = year;
116  * 	}
117  * }
118  * </pre>
119  </code>
120  * 
121  * 
122  * Our {@link RequestDispatcher} automatically set these parameter values from a given HTTP request. For example, an
123  * HTTP request
124  * 
125  * http://localhost:8989/hello.action?name=leo&year=2007
126  * 
127  * will invoke setName("leo") and setYear(2007) methods.
128  * 
129  * Note that, although the query string in the HTTP request consists of string values, our {@link BeanUtil} library
130  * detects the data type to which the string should be translated by analysing the class definition. In this example,
131  * the string value
132  * 
133  * "2007" is translated into an integer, 2007.
134  * 
135  * The web page result of the avobe request looks like as this:
136  * 
137  * <pre>
138  *  Hello leo(2007)
139  * </pre>
140  * 
141  * 
142  * Alternate access method: http://localhost:8989/dispathcer?actionClass=org.utgenome .gwt.utgb.server.app.hello&
143  * 
144  * @author leo
145  * 
146  */
147 public class RequestDispatcher implements Filter {
148 	/**
149 	 * 
150 	 */
151 	private static final long serialVersionUID = 1L;
152 	private static Logger _logger = Logger.getLogger(RequestDispatcher.class);
153 
154 	private LinkedList<RequestMap> requestMapList = new LinkedList<RequestMap>();
155 	private boolean isInGWTHostedMode = false;
156 	private static HashMap<String, String> mimeTypes = new HashMap<String, String>();
157 
158 	private void initMimeTypes() {
159 		if (!isInGWTHostedMode)
160 			return; // no need to initialize the mime type table other than the GWT Hosted mode
161 
162 		// the following part is borrowed from GWTShellServlet
163 		mimeTypes.put("abs", "audio/x-mpeg");
164 		mimeTypes.put("ai", "application/postscript");
165 		mimeTypes.put("aif", "audio/x-aiff");
166 		mimeTypes.put("aifc", "audio/x-aiff");
167 		mimeTypes.put("aiff", "audio/x-aiff");
168 		mimeTypes.put("aim", "application/x-aim");
169 		mimeTypes.put("art", "image/x-jg");
170 		mimeTypes.put("asf", "video/x-ms-asf");
171 		mimeTypes.put("asx", "video/x-ms-asf");
172 		mimeTypes.put("au", "audio/basic");
173 		mimeTypes.put("avi", "video/x-msvideo");
174 		mimeTypes.put("avx", "video/x-rad-screenplay");
175 		mimeTypes.put("bcpio", "application/x-bcpio");
176 		mimeTypes.put("bin", "application/octet-stream");
177 		mimeTypes.put("bmp", "image/bmp");
178 		mimeTypes.put("body", "text/html");
179 		mimeTypes.put("cdf", "application/x-cdf");
180 		mimeTypes.put("cer", "application/x-x509-ca-cert");
181 		mimeTypes.put("class", "application/java");
182 		mimeTypes.put("cpio", "application/x-cpio");
183 		mimeTypes.put("csh", "application/x-csh");
184 		mimeTypes.put("css", "text/css");
185 		mimeTypes.put("dib", "image/bmp");
186 		mimeTypes.put("doc", "application/msword");
187 		mimeTypes.put("dtd", "text/plain");
188 		mimeTypes.put("dv", "video/x-dv");
189 		mimeTypes.put("dvi", "application/x-dvi");
190 		mimeTypes.put("eps", "application/postscript");
191 		mimeTypes.put("etx", "text/x-setext");
192 		mimeTypes.put("exe", "application/octet-stream");
193 		mimeTypes.put("gif", "image/gif");
194 		mimeTypes.put("gtar", "application/x-gtar");
195 		mimeTypes.put("gz", "application/x-gzip");
196 		mimeTypes.put("hdf", "application/x-hdf");
197 		mimeTypes.put("hqx", "application/mac-binhex40");
198 		mimeTypes.put("htc", "text/x-component");
199 		mimeTypes.put("htm", "text/html");
200 		mimeTypes.put("html", "text/html");
201 		mimeTypes.put("hqx", "application/mac-binhex40");
202 		mimeTypes.put("ief", "image/ief");
203 		mimeTypes.put("jad", "text/vnd.sun.j2me.app-descriptor");
204 		mimeTypes.put("jar", "application/java-archive");
205 		mimeTypes.put("java", "text/plain");
206 		mimeTypes.put("jnlp", "application/x-java-jnlp-file");
207 		mimeTypes.put("jpe", "image/jpeg");
208 		mimeTypes.put("jpeg", "image/jpeg");
209 		mimeTypes.put("jpg", "image/jpeg");
210 		mimeTypes.put("js", "text/javascript");
211 		mimeTypes.put("jsf", "text/plain");
212 		mimeTypes.put("jspf", "text/plain");
213 		mimeTypes.put("kar", "audio/x-midi");
214 		mimeTypes.put("latex", "application/x-latex");
215 		mimeTypes.put("m3u", "audio/x-mpegurl");
216 		mimeTypes.put("mac", "image/x-macpaint");
217 		mimeTypes.put("man", "application/x-troff-man");
218 		mimeTypes.put("me", "application/x-troff-me");
219 		mimeTypes.put("mid", "audio/x-midi");
220 		mimeTypes.put("midi", "audio/x-midi");
221 		mimeTypes.put("mif", "application/x-mif");
222 		mimeTypes.put("mov", "video/quicktime");
223 		mimeTypes.put("movie", "video/x-sgi-movie");
224 		mimeTypes.put("mp1", "audio/x-mpeg");
225 		mimeTypes.put("mp2", "audio/x-mpeg");
226 		mimeTypes.put("mp3", "audio/x-mpeg");
227 		mimeTypes.put("mpa", "audio/x-mpeg");
228 		mimeTypes.put("mpe", "video/mpeg");
229 		mimeTypes.put("mpeg", "video/mpeg");
230 		mimeTypes.put("mpega", "audio/x-mpeg");
231 		mimeTypes.put("mpg", "video/mpeg");
232 		mimeTypes.put("mpv2", "video/mpeg2");
233 		mimeTypes.put("ms", "application/x-wais-source");
234 		mimeTypes.put("nc", "application/x-netcdf");
235 		mimeTypes.put("oda", "application/oda");
236 		mimeTypes.put("pbm", "image/x-portable-bitmap");
237 		mimeTypes.put("pct", "image/pict");
238 		mimeTypes.put("pdf", "application/pdf");
239 		mimeTypes.put("pgm", "image/x-portable-graymap");
240 		mimeTypes.put("pic", "image/pict");
241 		mimeTypes.put("pict", "image/pict");
242 		mimeTypes.put("pls", "audio/x-scpls");
243 		mimeTypes.put("png", "image/png");
244 		mimeTypes.put("pnm", "image/x-portable-anymap");
245 		mimeTypes.put("pnt", "image/x-macpaint");
246 		mimeTypes.put("ppm", "image/x-portable-pixmap");
247 		mimeTypes.put("ppt", "application/powerpoint");
248 		mimeTypes.put("ps", "application/postscript");
249 		mimeTypes.put("psd", "image/x-photoshop");
250 		mimeTypes.put("qt", "video/quicktime");
251 		mimeTypes.put("qti", "image/x-quicktime");
252 		mimeTypes.put("qtif", "image/x-quicktime");
253 		mimeTypes.put("ras", "image/x-cmu-raster");
254 		mimeTypes.put("rgb", "image/x-rgb");
255 		mimeTypes.put("rm", "application/vnd.rn-realmedia");
256 		mimeTypes.put("roff", "application/x-troff");
257 		mimeTypes.put("rtf", "application/rtf");
258 		mimeTypes.put("rtx", "text/richtext");
259 		mimeTypes.put("sh", "application/x-sh");
260 		mimeTypes.put("shar", "application/x-shar");
261 		mimeTypes.put("smf", "audio/x-midi");
262 		mimeTypes.put("sit", "application/x-stuffit");
263 		mimeTypes.put("snd", "audio/basic");
264 		mimeTypes.put("src", "application/x-wais-source");
265 		mimeTypes.put("sv4cpio", "application/x-sv4cpio");
266 		mimeTypes.put("sv4crc", "application/x-sv4crc");
267 		mimeTypes.put("swf", "application/x-shockwave-flash");
268 		mimeTypes.put("t", "application/x-troff");
269 		mimeTypes.put("tar", "application/x-tar");
270 		mimeTypes.put("tcl", "application/x-tcl");
271 		mimeTypes.put("tex", "application/x-tex");
272 		mimeTypes.put("texi", "application/x-texinfo");
273 		mimeTypes.put("texinfo", "application/x-texinfo");
274 		mimeTypes.put("tif", "image/tiff");
275 		mimeTypes.put("tiff", "image/tiff");
276 		mimeTypes.put("tr", "application/x-troff");
277 		mimeTypes.put("tsv", "text/tab-separated-values");
278 		mimeTypes.put("txt", "text/plain");
279 		mimeTypes.put("ulw", "audio/basic");
280 		mimeTypes.put("ustar", "application/x-ustar");
281 		mimeTypes.put("xbm", "image/x-xbitmap");
282 		mimeTypes.put("xht", "application/xhtml+xml");
283 		mimeTypes.put("xhtml", "application/xhtml+xml");
284 		mimeTypes.put("xml", "text/xml");
285 		mimeTypes.put("xpm", "image/x-xpixmap");
286 		mimeTypes.put("xsl", "text/xml");
287 		mimeTypes.put("xwd", "image/x-xwindowdump");
288 		mimeTypes.put("wav", "audio/x-wav");
289 		mimeTypes.put("svg", "image/svg+xml");
290 		mimeTypes.put("svgz", "image/svg+xml");
291 		mimeTypes.put("vsd", "application/x-visio");
292 		mimeTypes.put("wbmp", "image/vnd.wap.wbmp");
293 		mimeTypes.put("wml", "text/vnd.wap.wml");
294 		mimeTypes.put("wmlc", "application/vnd.wap.wmlc");
295 		mimeTypes.put("wmls", "text/vnd.wap.wmlscript");
296 		mimeTypes.put("wmlscriptc", "application/vnd.wap.wmlscriptc");
297 		mimeTypes.put("wrl", "x-world/x-vrml");
298 		mimeTypes.put("Z", "application/x-compress");
299 		mimeTypes.put("z", "application/x-compress");
300 		mimeTypes.put("zip", "application/zip");
301 	}
302 
303 	private String removeFirstSlash(String requestURI) {
304 		if (requestURI.startsWith("/"))
305 			return requestURI.substring(1);
306 		else
307 			return requestURI;
308 	}
309 
310 	public static String removeGWTModuleNamePart(String requestURI) {
311 		return requestURI.replaceFirst("\\/\\w+(\\.\\w+)*\\/", "");
312 	}
313 
314 	private static Pattern invalidCharInParamKey = Pattern.compile("[.-]");
315 
316 	public static void setRequestParametersToHandler(RequestHandler handler, HttpServletRequest req) {
317 		assert (handler != null);
318 
319 		// bind the parameter values to the RequestHandler
320 		try {
321 			Properties prop = new Properties();
322 			for (Enumeration e = req.getParameterNames(); e.hasMoreElements();) {
323 				String key = e.nextElement().toString();
324 				Matcher m = invalidCharInParamKey.matcher(key);
325 				String sanitizedKey = m.replaceAll("");
326 				prop.put(sanitizedKey, req.getParameter(key));
327 			}
328 			if (_logger.isTraceEnabled())
329 				_logger.trace(prop.toString());
330 			Lens.loadMap(handler, prop);
331 		}
332 		catch (XerialException e) {
333 			_logger.error(e);
334 		}
335 
336 	}
337 
338 	public void destroy() {
339 		_logger.info("destroy RequestDispatcher");
340 	}
341 
342 	public static String getExtension(String path) {
343 		int dotIndex = path.lastIndexOf(".");
344 		if (dotIndex > 0) {
345 			return path.substring(dotIndex + 1);
346 		}
347 		else
348 			return "";
349 	}
350 
351 	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
352 
353 		HttpServletRequest req = (HttpServletRequest) request;
354 		HttpServletResponse resp = (HttpServletResponse) response;
355 
356 		// resolve request URI
357 		String requestURI = null;
358 		if (isInGWTHostedMode) {
359 			// in GWT-shell, we have to remove the module name prefix in the
360 			// request URI.
361 			requestURI = req.getRequestURI();
362 			requestURI = removeGWTModuleNamePart(requestURI);
363 		}
364 		else
365 			requestURI = removeFirstSlash(req.getServletPath());
366 		_logger.trace("request URI: " + requestURI);
367 
368 		dispatchRequest(new RequestURI(requestURI), req, resp, filterChain);
369 	}
370 
371 	public void dispatchRequest(RequestURI requestURI, HttpServletRequest req, HttpServletResponse resp, FilterChain filterChain) throws IOException,
372 			ServletException {
373 
374 		for (RequestMap map : requestMapList) {
375 			RequestHandler handler = map.map(requestURI, req, resp);
376 			if (handler != null) {
377 				req.setAttribute("actionPrefix", requestURI.getPrefix());
378 				req.setAttribute("actionSuffix", requestURI.getSuffix());
379 				setRequestParametersToHandler(handler, req);
380 
381 				// validate the request parameters
382 				try {
383 					handler.validate(req, resp);
384 				}
385 				catch (UTGBException e) {
386 					_logger.error(e);
387 					return;
388 				}
389 
390 				// passes the request to the handler
391 				handler.handle(req, resp);
392 
393 				// returns without chains the request
394 				return;
395 			}
396 		}
397 
398 		// when no handler is found
399 
400 		// search src/main/webapp folder (in GWT hosted mode)
401 		if (doGetFromSrcMainWebApp(requestURI.getURI(), req, resp))
402 			return;
403 
404 		// when no handler is found for the requested URI, forward this
405 		// request to the next chain filter
406 		filterChain.doFilter(req, resp);
407 	}
408 
409 	private static DateFormat sHttpDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss", Locale.US);
410 
411 	/**
412 	 * Converts a file-style date/time into a string form that is compatible with HTTP.
413 	 */
414 	public static String toInternetDateFormat(long time) {
415 		Date date = dateToGMT(new Date(time));
416 		String dateGmt;
417 		synchronized (sHttpDateFormat) {
418 			dateGmt = sHttpDateFormat.format(date) + " GMT";
419 		}
420 		return dateGmt;
421 	}
422 
423 	/**
424 	 * Converts a local date to GMT.
425 	 * 
426 	 * @param date
427 	 *            the date in the local time zone
428 	 * @return the GMT version
429 	 */
430 	private static Date dateToGMT(Date date) {
431 		Calendar cal = Calendar.getInstance();
432 		long tzMillis = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
433 		return new Date(date.getTime() - tzMillis);
434 	}
435 
436 	private boolean doGetFromSrcMainWebApp(String requestURI, ServletRequest request, ServletResponse response) throws IOException {
437 
438 		if (!isInGWTHostedMode)
439 			return false;
440 
441 		// search the resource
442 		File resourceFile = new File("src/main/webapp/", requestURI);
443 		if (!resourceFile.exists())
444 			return false;
445 
446 		// Get the MIME type.
447 		String mimeType = mimeTypes.get(getExtension(requestURI));
448 		if (mimeType == null) {
449 			mimeType = "application/octet-stream";
450 		}
451 
452 		HttpServletResponse resp = (HttpServletResponse) response;
453 		resp.setStatus(HttpServletResponse.SC_OK);
454 
455 		// set cache time
456 		long cacheTime = 5;
457 		long expires = 24 * 1000;
458 		resp.setHeader("Cache-Control", "max-age=" + cacheTime);
459 		String expiresString = toInternetDateFormat(expires);
460 		resp.setHeader("Expires", expiresString);
461 
462 		// set current date
463 		long now = new Date().getTime();
464 		resp.setHeader("Date", toInternetDateFormat(now));
465 
466 		// content type
467 		resp.setContentType(mimeType);
468 
469 		// last modified time
470 		long lastModified = resourceFile.lastModified();
471 		String lastModifiedStr = toInternetDateFormat(resourceFile.lastModified());
472 		resp.setHeader("Last-Modified", lastModifiedStr);
473 
474 		// content length
475 		long contentLength = resourceFile.length();
476 		if (contentLength >= 0) {
477 			resp.setHeader("Content-Length", Long.toString(contentLength));
478 		}
479 
480 		// Send the bytes.
481 		OutputStream out = resp.getOutputStream();
482 		BufferedInputStream in = new BufferedInputStream(new FileInputStream(resourceFile));
483 		byte[] buf = new byte[1024];
484 		int bytesRead = 0;
485 		while ((bytesRead = in.read(buf)) >= 0) {
486 			out.write(buf, 0, bytesRead);
487 		}
488 		in.close();
489 		return true;
490 	}
491 
492 	public static boolean isGWTHostedMode() {
493 		return Boolean.parseBoolean(System.getProperty("gwt-hosted-mode"));
494 	}
495 
496 	public void init(FilterConfig config) throws ServletException {
497 		_logger.info("initializing RequestDispatcher");
498 
499 		isInGWTHostedMode = isGWTHostedMode();
500 
501 		if (isInGWTHostedMode) {
502 			_logger.info("GWT Hosted Mode");
503 			initMimeTypes();
504 		}
505 
506 		initRequestMap();
507 	}
508 
509 	public void initRequestMap() {
510 		addRequestMap(new DefaultRequestMap());
511 	}
512 
513 	public void addRequestMap(RequestMap map) {
514 		requestMapList.add(map);
515 	}
516 
517 }