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 }