001 /* 002 * $Id: TemplateServlet.java,v 1.16 2005/06/05 08:16:07 cstein Exp $ 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.BufferedReader; 046 import java.io.File; 047 import java.io.FileReader; 048 import java.io.IOException; 049 import java.io.StringReader; 050 import java.io.Writer; 051 import java.util.Map; 052 import java.util.WeakHashMap; 053 054 import javax.servlet.ServletConfig; 055 import javax.servlet.ServletException; 056 import javax.servlet.http.HttpServletRequest; 057 import javax.servlet.http.HttpServletResponse; 058 059 /** 060 * A generic servlet for serving (mostly HTML) templates. 061 * 062 * It wraps a <code>groovy.text.TemplateEngine</code> to process HTTP 063 * requests. By default, it uses the 064 * <code>groovy.text.SimpleTemplateEngine</code> which interprets JSP-like (or 065 * Canvas-like) templates. The init parameter <code>templateEngine</code> 066 * defines the fully qualified class name of the template to use.<br> 067 * 068 * <p> 069 * Headless <code>helloworld.html</code> example 070 * <pre><code> 071 * <html> 072 * <body> 073 * <% 3.times { %> 074 * Hello World! 075 * <% } %> 076 * <br> 077 * session.id = ${session.id} 078 * </body> 079 * </html> 080 * </code></pre> 081 * </p> 082 * 083 * @see TemplateServlet#setVariables(ServletBinding) 084 * 085 * @author Christian Stein 086 * @author Guillaume Laforge 087 * @version 2.0 088 */ 089 public class TemplateServlet extends AbstractHttpServlet { 090 091 /** 092 * Simple cache entry that validates against last modified and length 093 * attributes of the specified file. 094 * 095 * @author Sormuras 096 */ 097 private static class TemplateCacheEntry { 098 099 long lastModified; 100 long length; 101 Template template; 102 103 public TemplateCacheEntry(File file, Template template) { 104 if (file == null) { 105 throw new NullPointerException("file"); 106 } 107 if (template == null) { 108 throw new NullPointerException("template"); 109 } 110 this.lastModified = file.lastModified(); 111 this.length = file.length(); 112 this.template = template; 113 } 114 115 /** 116 * Checks the passed file attributes against those cached ones. 117 * 118 * @param file 119 * Other file handle to compare to the cached values. 120 * @return <code>true</code> if all measured values match, else <code>false</code> 121 */ 122 public boolean validate(File file) { 123 if (file == null) { 124 throw new NullPointerException("file"); 125 } 126 if (file.lastModified() != this.lastModified) { 127 return false; 128 } 129 if (file.length() != this.length) { 130 return false; 131 } 132 return true; 133 } 134 135 } 136 137 /* 138 * Enables more log statements. 139 */ 140 private static final boolean VERBOSE = true; 141 142 /** 143 * Simple file name to template cache map. 144 */ 145 // Java5 private final Map<String, TemplateCacheEntry> cache; 146 private final Map cache; 147 148 /** 149 * Underlying template engine used to evaluate template source files. 150 */ 151 private TemplateEngine engine; 152 153 /** 154 * Flag that controls the appending of the "Generated by ..." comment. 155 */ 156 private boolean generatedBy; 157 158 /** 159 * Create new TemplateSerlvet. 160 */ 161 public TemplateServlet() { 162 // Java 5 this.cache = new WeakHashMap<String, TemplateCacheEntry>(); 163 this.cache = new WeakHashMap(); 164 //this.context = null; // assigned later by super.init() 165 this.engine = null; // assigned later by init() 166 this.generatedBy = true; // may be changed by init() 167 } 168 169 /** 170 * Triggers the template creation eliminating all new line characters. 171 * 172 * Its a work around! New lines should cause troubles when compiling. But 173 * sometimes(?) the do: http://jira.codehaus.org/browse/GROOVY-818 174 * See FIXME note around line 250, where this method is called. 175 * 176 * @see TemplateServlet#getTemplate(File) 177 * @see BufferedReader#readLine() 178 */ 179 private Template createTemplate(int bufferCapacity, FileReader fileReader) 180 throws Exception { 181 StringBuffer sb = new StringBuffer(bufferCapacity); 182 BufferedReader reader = new BufferedReader(fileReader); 183 try { 184 String line = reader.readLine(); 185 while (line != null) { 186 sb.append(line); 187 //if (VERBOSE) { // prints the entire source file 188 // log(" | " + line); 189 //} 190 line = reader.readLine(); 191 } 192 } 193 finally { 194 if (reader != null) { 195 reader.close(); 196 } 197 } 198 StringReader stringReader = new StringReader(sb.toString()); 199 Template template = engine.createTemplate(stringReader); 200 stringReader.close(); 201 return template; 202 } 203 204 /** 205 * Gets the template created by the underlying engine parsing the request. 206 * 207 * <p> 208 * This method looks up a simple (weak) hash map for an existing template 209 * object that matches the source file. If the source file didn't change in 210 * length and its last modified stamp hasn't changed compared to a precompiled 211 * template object, this template is used. Otherwise, there is no or an 212 * invalid template object cache entry, a new one is created by the underlying 213 * template engine. This new instance is put to the cache for consecutive 214 * calls. 215 * </p> 216 * 217 * @return The template that will produce the response text. 218 * @param file 219 * The HttpServletRequest. 220 * @throws IOException 221 * If the request specified an invalid template source file 222 */ 223 protected Template getTemplate(File file) throws ServletException { 224 225 String key = file.getAbsolutePath(); 226 Template template = null; 227 228 // 229 // Test cache for a valid template bound to the key. 230 // 231 TemplateCacheEntry entry = (TemplateCacheEntry) cache.get(key); 232 if (entry != null) { 233 if (entry.validate(file)) { // log("Valid cache hit! :)"); 234 template = entry.template; 235 } // else log("Cached template needs recompiliation!"); 236 } // else log("Cache miss."); 237 238 // 239 // Template not cached or the source file changed - compile new template! 240 // 241 if (template == null) { 242 if (VERBOSE) { 243 log("Creating new template from file " + file + "..."); 244 } 245 FileReader reader = null; 246 try { 247 reader = new FileReader(file); 248 // 249 // FIXME Template creation should eliminate '\n' by default?! 250 // 251 // template = engine.createTemplate(reader); 252 // 253 // General error during parsing: 254 // expecting anything but ''\n''; got it anyway 255 // 256 template = createTemplate((int) file.length(), reader); 257 } 258 catch (Exception e) { 259 throw new ServletException("Creation of template failed: " + e, e); 260 } 261 finally { 262 if (reader != null) { 263 try { 264 reader.close(); 265 } 266 catch (IOException ignore) { 267 // e.printStackTrace(); 268 } 269 } 270 } 271 cache.put(key, new TemplateCacheEntry(file, template)); 272 if (VERBOSE) { 273 log("Created and added template to cache. [key=" + key + "]"); 274 } 275 } 276 277 // 278 // Last sanity check. 279 // 280 if (template == null) { 281 throw new ServletException("Template is null? Should not happen here!"); 282 } 283 284 return template; 285 286 } 287 288 /** 289 * Initializes the servlet from hints the container passes. 290 * <p> 291 * Delegates to sub-init methods and parses the following parameters: 292 * <ul> 293 * <li> <tt>"generatedBy"</tt> : boolean, appends "Generated by ..." to the 294 * HTML response text generated by this servlet. 295 * </li> 296 * </ul> 297 * @param config 298 * Passed by the servlet container. 299 * @throws ServletException 300 * if this method encountered difficulties 301 * 302 * @see TemplateServlet#initTemplateEngine(ServletConfig) 303 */ 304 public void init(ServletConfig config) throws ServletException { 305 super.init(config); 306 this.engine = initTemplateEngine(config); 307 if (engine == null) { 308 throw new ServletException("Template engine not instantiated."); 309 } 310 String value = config.getInitParameter("generatedBy"); 311 if (value != null) { 312 this.generatedBy = Boolean.valueOf(value).booleanValue(); 313 } 314 if (VERBOSE) { 315 log(getClass().getName() + " initialized on " + engine.getClass()); 316 } 317 } 318 319 /** 320 * Creates the template engine. 321 * 322 * Called by {@link TemplateServlet#init(ServletConfig)} and returns just 323 * <code>new groovy.text.SimpleTemplateEngine()</code> if the init parameter 324 * <code>templateEngine</code> is not set by the container configuration. 325 * 326 * @param config 327 * Current serlvet configuration passed by the container. 328 * 329 * @return The underlying template engine or <code>null</code> on error. 330 * 331 * @see TemplateServlet#initTemplateEngine(javax.servlet.ServletConfig) 332 */ 333 protected TemplateEngine initTemplateEngine(ServletConfig config) { 334 String name = config.getInitParameter("templateEngine"); 335 if (name == null) { 336 return new SimpleTemplateEngine(); 337 } 338 try { 339 return (TemplateEngine) Class.forName(name).newInstance(); 340 } 341 catch (InstantiationException e) { 342 log("Could not instantiate template engine: " + name, e); 343 } 344 catch (IllegalAccessException e) { 345 log("Could not access template engine class: " + name, e); 346 } 347 catch (ClassNotFoundException e) { 348 log("Could not find template engine class: " + name, e); 349 } 350 return null; 351 } 352 353 /** 354 * Services the request with a response. 355 * <p> 356 * First the request is parsed for the source file uri. If the specified file 357 * could not be found or can not be read an error message is sent as response. 358 * 359 * </p> 360 * @param request 361 * The http request. 362 * @param response 363 * The http response. 364 * @throws IOException 365 * if an input or output error occurs while the servlet is 366 * handling the HTTP request 367 * @throws ServletException 368 * if the HTTP request cannot be handled 369 */ 370 public void service(HttpServletRequest request, 371 HttpServletResponse response) throws ServletException, IOException { 372 373 if (VERBOSE) { 374 log("Creating/getting cached template..."); 375 } 376 377 // 378 // Get the template source file handle. 379 // 380 File file = super.getScriptUriAsFile(request); 381 if (!file.exists()) { 382 response.sendError(HttpServletResponse.SC_NOT_FOUND); 383 return; // throw new IOException(file.getAbsolutePath()); 384 } 385 if (!file.canRead()) { 386 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Can not read!"); 387 return; // throw new IOException(file.getAbsolutePath()); 388 } 389 390 // 391 // Get the requested template. 392 // 393 long getMillis = System.currentTimeMillis(); 394 Template template = getTemplate(file); 395 getMillis = System.currentTimeMillis() - getMillis; 396 397 // 398 // Create new binding for the current request. 399 // 400 ServletBinding binding = new ServletBinding(request, response, servletContext); 401 setVariables(binding); 402 403 // 404 // Prepare the response buffer content type _before_ getting the writer. 405 // 406 response.setContentType(CONTENT_TYPE_TEXT_HTML); 407 408 // 409 // Get the output stream writer from the binding. 410 // 411 Writer out = (Writer) binding.getVariable("out"); 412 if (out == null) { 413 out = response.getWriter(); 414 } 415 416 // 417 // Evaluate the template. 418 // 419 if (VERBOSE) { 420 log("Making template..."); 421 } 422 // String made = template.make(binding.getVariables()).toString(); 423 // log(" = " + made); 424 long makeMillis = System.currentTimeMillis(); 425 template.make(binding.getVariables()).writeTo(out); 426 makeMillis = System.currentTimeMillis() - makeMillis; 427 428 if (generatedBy) { 429 StringBuffer sb = new StringBuffer(100); 430 sb.append("\n<!-- Generated by Groovy TemplateServlet [create/get="); 431 sb.append(Long.toString(getMillis)); 432 sb.append(" ms, make="); 433 sb.append(Long.toString(makeMillis)); 434 sb.append(" ms] -->\n"); 435 out.write(sb.toString()); 436 } 437 438 // 439 // Set status code and flush the response buffer. 440 // 441 response.setStatus(HttpServletResponse.SC_OK); 442 response.flushBuffer(); 443 444 if (VERBOSE) { 445 log("Template request responded. [create/get=" + getMillis 446 + " ms, make=" + makeMillis + " ms]"); 447 } 448 449 } 450 451 /** 452 * Override this method to set your variables to the Groovy binding. 453 * <p> 454 * All variables bound the binding are passed to the template source text, 455 * e.g. the HTML file, when the template is merged. 456 * </p> 457 * <p> 458 * The binding provided by TemplateServlet does already include some default 459 * variables. As of this writing, they are (copied from 460 * {@link groovy.servlet.ServletBinding}): 461 * <ul> 462 * <li><tt>"request"</tt> : HttpServletRequest </li> 463 * <li><tt>"response"</tt> : HttpServletResponse </li> 464 * <li><tt>"context"</tt> : ServletContext </li> 465 * <li><tt>"application"</tt> : ServletContext </li> 466 * <li><tt>"session"</tt> : request.getSession(true) </li> 467 * </ul> 468 * </p> 469 * <p> 470 * And via explicit hard-coded keywords: 471 * <ul> 472 * <li><tt>"out"</tt> : response.getWriter() </li> 473 * <li><tt>"sout"</tt> : response.getOutputStream() </li> 474 * <li><tt>"html"</tt> : new MarkupBuilder(response.getWriter()) </li> 475 * </ul> 476 * </p> 477 * 478 * <p>Example binding all servlet context variables: 479 * <pre><code> 480 * class Mytlet extends TemplateServlet { 481 * 482 * private ServletContext context; 483 * 484 * public void init(ServletConfig config) { 485 * this.context = config.getServletContext(); 486 * } 487 * 488 * protected void setVariables(ServletBinding binding) { 489 * Enumeration enumeration = context.getAttributeNames(); 490 * while (enumeration.hasMoreElements()) { 491 * String name = (String) enumeration.nextElement(); 492 * binding.setVariable(name, context.getAttribute(name)); 493 * } 494 * } 495 * 496 * } 497 * <code></pre> 498 * </p> 499 * 500 * @param binding 501 * to get modified 502 * 503 * @see TemplateServlet 504 */ 505 protected void setVariables(ServletBinding binding) { 506 // empty 507 } 508 509 }