001 /* 002 * $Id: TemplateServlet.java 4032 2006-08-30 07:18:49Z mguillem $ 003 * 004 * Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved. 005 * 006 * Redistribution and use of this software and associated documentation 007 * ("Software"), with or without modification, are permitted provided that the 008 * following conditions are met: 009 * 010 * 1. Redistributions of source code must retain copyright statements and 011 * notices. Redistributions must also contain a copy of this document. 012 * 013 * 2. Redistributions in binary form must reproduce the above copyright notice, 014 * this list of conditions and the following disclaimer in the documentation 015 * and/or other materials provided with the distribution. 016 * 017 * 3. The name "groovy" must not be used to endorse or promote products derived 018 * from this Software without prior written permission of The Codehaus. For 019 * written permission, please contact info@codehaus.org. 020 * 021 * 4. Products derived from this Software may not be called "groovy" nor may 022 * "groovy" appear in their names without prior written permission of The 023 * Codehaus. "groovy" is a registered trademark of The Codehaus. 024 * 025 * 5. Due credit should be given to The Codehaus - http://groovy.codehaus.org/ 026 * 027 * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY 028 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 029 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 030 * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR 031 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 032 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 033 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 034 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 035 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 036 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 037 * 038 */ 039 package groovy.servlet; 040 041 import groovy.text.SimpleTemplateEngine; 042 import groovy.text.Template; 043 import groovy.text.TemplateEngine; 044 045 import java.io.File; 046 import java.io.FileReader; 047 import java.io.IOException; 048 import java.io.Writer; 049 import java.util.Date; 050 import java.util.Map; 051 import java.util.WeakHashMap; 052 053 import javax.servlet.ServletConfig; 054 import javax.servlet.ServletException; 055 import javax.servlet.http.HttpServletRequest; 056 import javax.servlet.http.HttpServletResponse; 057 058 /** 059 * A generic servlet for serving (mostly HTML) templates. 060 * 061 * <p> 062 * It delegates work to a <code>groovy.text.TemplateEngine</code> implementation 063 * processing HTTP requests. 064 * 065 * <h4>Usage</h4> 066 * 067 * <code>helloworld.html</code> is a headless HTML-like template 068 * <pre><code> 069 * <html> 070 * <body> 071 * <% 3.times { %> 072 * Hello World! 073 * <% } %> 074 * <br> 075 * </body> 076 * </html> 077 * </code></pre> 078 * 079 * Minimal <code>web.xml</code> example serving HTML-like templates 080 * <pre><code> 081 * <web-app> 082 * <servlet> 083 * <servlet-name>template</servlet-name> 084 * <servlet-class>groovy.servlet.TemplateServlet</servlet-class> 085 * </servlet> 086 * <servlet-mapping> 087 * <servlet-name>template</servlet-name> 088 * <url-pattern>*.html</url-pattern> 089 * </servlet-mapping> 090 * </web-app> 091 * </code></pre> 092 * 093 * <h4>Template engine configuration</h4> 094 * 095 * <p> 096 * By default, the TemplateServer uses the {@link groovy.text.SimpleTemplateEngine} 097 * which interprets JSP-like templates. The init parameter <code>template.engine</code> 098 * defines the fully qualified class name of the template to use: 099 * <pre> 100 * template.engine = [empty] - equals groovy.text.SimpleTemplateEngine 101 * template.engine = groovy.text.SimpleTemplateEngine 102 * template.engine = groovy.text.GStringTemplateEngine 103 * template.engine = groovy.text.XmlTemplateEngine 104 * </pre> 105 * 106 * <h4>Logging and extra-output options</h4> 107 * 108 * <p> 109 * This implementation provides a verbosity flag switching log statements. 110 * The servlet init parameter name is: 111 * <pre> 112 * generate.by = true(default) | false 113 * </pre> 114 * 115 * @see TemplateServlet#setVariables(ServletBinding) 116 * 117 * @author Christian Stein 118 * @author Guillaume Laforge 119 * @version 2.0 120 */ 121 public class TemplateServlet extends AbstractHttpServlet { 122 123 /** 124 * Simple cache entry that validates against last modified and length 125 * attributes of the specified file. 126 * 127 * @author Christian Stein 128 */ 129 private static class TemplateCacheEntry { 130 131 Date date; 132 long hit; 133 long lastModified; 134 long length; 135 Template template; 136 137 public TemplateCacheEntry(File file, Template template) { 138 this(file, template, false); // don't get time millis for sake of speed 139 } 140 141 public TemplateCacheEntry(File file, Template template, boolean timestamp) { 142 if (file == null) { 143 throw new NullPointerException("file"); 144 } 145 if (template == null) { 146 throw new NullPointerException("template"); 147 } 148 if (timestamp) { 149 this.date = new Date(System.currentTimeMillis()); 150 } else { 151 this.date = null; 152 } 153 this.hit = 0; 154 this.lastModified = file.lastModified(); 155 this.length = file.length(); 156 this.template = template; 157 } 158 159 /** 160 * Checks the passed file attributes against those cached ones. 161 * 162 * @param file 163 * Other file handle to compare to the cached values. 164 * @return <code>true</code> if all measured values match, else <code>false</code> 165 */ 166 public boolean validate(File file) { 167 if (file == null) { 168 throw new NullPointerException("file"); 169 } 170 if (file.lastModified() != this.lastModified) { 171 return false; 172 } 173 if (file.length() != this.length) { 174 return false; 175 } 176 hit++; 177 return true; 178 } 179 180 public String toString() { 181 if (date == null) { 182 return "Hit #" + hit; 183 } 184 return "Hit #" + hit + " since " + date; 185 } 186 187 } 188 189 /** 190 * Simple file name to template cache map. 191 */ 192 private final Map cache; 193 194 /** 195 * Underlying template engine used to evaluate template source files. 196 */ 197 private TemplateEngine engine; 198 199 /** 200 * Flag that controls the appending of the "Generated by ..." comment. 201 */ 202 private boolean generateBy; 203 204 /** 205 * Create new TemplateSerlvet. 206 */ 207 public TemplateServlet() { 208 this.cache = new WeakHashMap(); 209 this.engine = null; // assigned later by init() 210 this.generateBy = true; // may be changed by init() 211 } 212 213 /** 214 * Gets the template created by the underlying engine parsing the request. 215 * 216 * <p> 217 * This method looks up a simple (weak) hash map for an existing template 218 * object that matches the source file. If the source file didn't change in 219 * length and its last modified stamp hasn't changed compared to a precompiled 220 * template object, this template is used. Otherwise, there is no or an 221 * invalid template object cache entry, a new one is created by the underlying 222 * template engine. This new instance is put to the cache for consecutive 223 * calls. 224 * </p> 225 * 226 * @return The template that will produce the response text. 227 * @param file 228 * The HttpServletRequest. 229 * @throws ServletException 230 * If the request specified an invalid template source file 231 */ 232 protected Template getTemplate(File file) throws ServletException { 233 234 String key = file.getAbsolutePath(); 235 Template template = null; 236 237 /* 238 * Test cache for a valid template bound to the key. 239 */ 240 if (verbose) { 241 log("Looking for cached template by key \"" + key + "\""); 242 } 243 TemplateCacheEntry entry = (TemplateCacheEntry) cache.get(key); 244 if (entry != null) { 245 if (entry.validate(file)) { 246 if (verbose) { 247 log("Cache hit! " + entry); 248 } 249 template = entry.template; 250 } else { 251 if (verbose) { 252 log("Cached template needs recompiliation!"); 253 } 254 } 255 } else { 256 if (verbose) { 257 log("Cache miss."); 258 } 259 } 260 261 // 262 // Template not cached or the source file changed - compile new template! 263 // 264 if (template == null) { 265 if (verbose) { 266 log("Creating new template from file " + file + "..."); 267 } 268 FileReader reader = null; 269 try { 270 reader = new FileReader(file); 271 template = engine.createTemplate(reader); 272 } catch (Exception e) { 273 throw new ServletException("Creation of template failed: " + e, e); 274 } finally { 275 if (reader != null) { 276 try { 277 reader.close(); 278 } catch (IOException ignore) { 279 // e.printStackTrace(); 280 } 281 } 282 } 283 cache.put(key, new TemplateCacheEntry(file, template, verbose)); 284 if (verbose) { 285 log("Created and added template to cache. [key=" + key + "]"); 286 } 287 } 288 289 // 290 // Last sanity check. 291 // 292 if (template == null) { 293 throw new ServletException("Template is null? Should not happen here!"); 294 } 295 296 return template; 297 298 } 299 300 /** 301 * Initializes the servlet from hints the container passes. 302 * <p> 303 * Delegates to sub-init methods and parses the following parameters: 304 * <ul> 305 * <li> <tt>"generatedBy"</tt> : boolean, appends "Generated by ..." to the 306 * HTML response text generated by this servlet. 307 * </li> 308 * </ul> 309 * @param config 310 * Passed by the servlet container. 311 * @throws ServletException 312 * if this method encountered difficulties 313 * 314 * @see TemplateServlet#initTemplateEngine(ServletConfig) 315 */ 316 public void init(ServletConfig config) throws ServletException { 317 super.init(config); 318 this.engine = initTemplateEngine(config); 319 if (engine == null) { 320 throw new ServletException("Template engine not instantiated."); 321 } 322 String value = config.getInitParameter("generated.by"); 323 if (value != null) { 324 this.generateBy = Boolean.valueOf(value).booleanValue(); 325 } 326 log("Servlet " + getClass().getName() + " initialized on " + engine.getClass()); 327 } 328 329 /** 330 * Creates the template engine. 331 * 332 * Called by {@link TemplateServlet#init(ServletConfig)} and returns just 333 * <code>new groovy.text.SimpleTemplateEngine()</code> if the init parameter 334 * <code>template.engine</code> is not set by the container configuration. 335 * 336 * @param config 337 * Current serlvet configuration passed by the container. 338 * 339 * @return The underlying template engine or <code>null</code> on error. 340 */ 341 protected TemplateEngine initTemplateEngine(ServletConfig config) { 342 String name = config.getInitParameter("template.engine"); 343 if (name == null) { 344 return new SimpleTemplateEngine(); 345 } 346 try { 347 return (TemplateEngine) Class.forName(name).newInstance(); 348 } catch (InstantiationException e) { 349 log("Could not instantiate template engine: " + name, e); 350 } catch (IllegalAccessException e) { 351 log("Could not access template engine class: " + name, e); 352 } catch (ClassNotFoundException e) { 353 log("Could not find template engine class: " + name, e); 354 } 355 return null; 356 } 357 358 /** 359 * Services the request with a response. 360 * <p> 361 * First the request is parsed for the source file uri. If the specified file 362 * could not be found or can not be read an error message is sent as response. 363 * 364 * </p> 365 * @param request 366 * The http request. 367 * @param response 368 * The http response. 369 * @throws IOException 370 * if an input or output error occurs while the servlet is 371 * handling the HTTP request 372 * @throws ServletException 373 * if the HTTP request cannot be handled 374 */ 375 public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 376 377 if (verbose) { 378 log("Creating/getting cached template..."); 379 } 380 381 // 382 // Get the template source file handle. 383 // 384 File file = super.getScriptUriAsFile(request); 385 String name = file.getName(); 386 if (!file.exists()) { 387 response.sendError(HttpServletResponse.SC_NOT_FOUND); 388 return; // throw new IOException(file.getAbsolutePath()); 389 } 390 if (!file.canRead()) { 391 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Can not read \"" + name + "\"!"); 392 return; // throw new IOException(file.getAbsolutePath()); 393 } 394 395 // 396 // Get the requested template. 397 // 398 long getMillis = System.currentTimeMillis(); 399 Template template = getTemplate(file); 400 getMillis = System.currentTimeMillis() - getMillis; 401 402 // 403 // Create new binding for the current request. 404 // 405 ServletBinding binding = new ServletBinding(request, response, servletContext); 406 setVariables(binding); 407 408 // 409 // Prepare the response buffer content type _before_ getting the writer. 410 // and set status code to ok 411 // 412 response.setContentType(CONTENT_TYPE_TEXT_HTML); 413 response.setStatus(HttpServletResponse.SC_OK); 414 415 // 416 // Get the output stream writer from the binding. 417 // 418 Writer out = (Writer) binding.getVariable("out"); 419 if (out == null) { 420 out = response.getWriter(); 421 } 422 423 // 424 // Evaluate the template. 425 // 426 if (verbose) { 427 log("Making template \"" + name + "\"..."); 428 } 429 // String made = template.make(binding.getVariables()).toString(); 430 // log(" = " + made); 431 long makeMillis = System.currentTimeMillis(); 432 template.make(binding.getVariables()).writeTo(out); 433 makeMillis = System.currentTimeMillis() - makeMillis; 434 435 if (generateBy) { 436 StringBuffer sb = new StringBuffer(100); 437 sb.append("\n<!-- Generated by Groovy TemplateServlet [create/get="); 438 sb.append(Long.toString(getMillis)); 439 sb.append(" ms, make="); 440 sb.append(Long.toString(makeMillis)); 441 sb.append(" ms] -->\n"); 442 out.write(sb.toString()); 443 } 444 445 // 446 // flush the response buffer. 447 // 448 response.flushBuffer(); 449 450 if (verbose) { 451 log("Template \"" + name + "\" request responded. [create/get=" + getMillis + " ms, make=" + makeMillis + " ms]"); 452 } 453 454 } 455 456 /** 457 * Override this method to set your variables to the Groovy binding. 458 * <p> 459 * All variables bound the binding are passed to the template source text, 460 * e.g. the HTML file, when the template is merged. 461 * </p> 462 * <p> 463 * The binding provided by TemplateServlet does already include some default 464 * variables. As of this writing, they are (copied from 465 * {@link groovy.servlet.ServletBinding}): 466 * <ul> 467 * <li><tt>"request"</tt> : HttpServletRequest </li> 468 * <li><tt>"response"</tt> : HttpServletResponse </li> 469 * <li><tt>"context"</tt> : ServletContext </li> 470 * <li><tt>"application"</tt> : ServletContext </li> 471 * <li><tt>"session"</tt> : request.getSession(<b>false</b>) </li> 472 * </ul> 473 * </p> 474 * <p> 475 * And via implicite hard-coded keywords: 476 * <ul> 477 * <li><tt>"out"</tt> : response.getWriter() </li> 478 * <li><tt>"sout"</tt> : response.getOutputStream() </li> 479 * <li><tt>"html"</tt> : new MarkupBuilder(response.getWriter()) </li> 480 * </ul> 481 * </p> 482 * 483 * <p>Example binding all servlet context variables: 484 * <pre><code> 485 * class Mytlet extends TemplateServlet { 486 * 487 * protected void setVariables(ServletBinding binding) { 488 * // Bind a simple variable 489 * binding.setVariable("answer", new Long(42)); 490 * 491 * // Bind all servlet context attributes... 492 * ServletContext context = (ServletContext) binding.getVariable("context"); 493 * Enumeration enumeration = context.getAttributeNames(); 494 * while (enumeration.hasMoreElements()) { 495 * String name = (String) enumeration.nextElement(); 496 * binding.setVariable(name, context.getAttribute(name)); 497 * } 498 * } 499 * 500 * } 501 * <code></pre> 502 * </p> 503 * 504 * @param binding 505 * to be modified 506 */ 507 protected void setVariables(ServletBinding binding) { 508 // empty 509 } 510 511 }