View Javadoc

1   /*
2    * $Id: TemplateServlet.java,v 1.21 2006/06/06 14:28:44 blackdrag Exp $
3    * 
4    * Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
5    * 
6    * Redistribution and use of this software and associated documentation
7    * ("Software"), with or without modification, are permitted provided that the
8    * following conditions are met:
9    * 
10   * 1. Redistributions of source code must retain copyright statements and
11   * notices. Redistributions must also contain a copy of this document.
12   * 
13   * 2. Redistributions in binary form must reproduce the above copyright notice,
14   * this list of conditions and the following disclaimer in the documentation
15   * and/or other materials provided with the distribution.
16   * 
17   * 3. The name "groovy" must not be used to endorse or promote products derived
18   * from this Software without prior written permission of The Codehaus. For
19   * written permission, please contact info@codehaus.org.
20   * 
21   * 4. Products derived from this Software may not be called "groovy" nor may
22   * "groovy" appear in their names without prior written permission of The
23   * Codehaus. "groovy" is a registered trademark of The Codehaus.
24   * 
25   * 5. Due credit should be given to The Codehaus - http://groovy.codehaus.org/
26   * 
27   * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY
28   * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
29   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
30   * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR
31   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
32   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
33   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
34   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
35   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37   *  
38   */
39  package groovy.servlet;
40  
41  import groovy.text.SimpleTemplateEngine;
42  import groovy.text.Template;
43  import groovy.text.TemplateEngine;
44  
45  import java.io.File;
46  import java.io.FileReader;
47  import java.io.IOException;
48  import java.io.Writer;
49  import java.util.Date;
50  import java.util.Map;
51  import java.util.WeakHashMap;
52  
53  import javax.servlet.ServletConfig;
54  import javax.servlet.ServletException;
55  import javax.servlet.http.HttpServletRequest;
56  import javax.servlet.http.HttpServletResponse;
57  
58  /***
59   * A generic servlet for serving (mostly HTML) templates.
60   * 
61   * <p>
62   * It delegates work to a <code>groovy.text.TemplateEngine</code> implementation 
63   * processing HTTP requests.
64   *
65   * <h4>Usage</h4>
66   * 
67   * <code>helloworld.html</code> is a headless HTML-like template
68   * <pre><code>
69   *  &lt;html&gt;
70   *    &lt;body&gt;
71   *      &lt;% 3.times { %&gt;
72   *        Hello World!
73   *      &lt;% } %&gt;
74   *      &lt;br&gt;
75   *    &lt;/body&gt;
76   *  &lt;/html&gt; 
77   * </code></pre>
78   * 
79   * Minimal <code>web.xml</code> example serving HTML-like templates
80   * <pre><code>
81   * &lt;web-app&gt;
82   *   &lt;servlet&gt;
83   *     &lt;servlet-name&gt;template&lt;/servlet-name&gt;
84   *     &lt;servlet-class&gt;groovy.servlet.TemplateServlet&lt;/servlet-class&gt;
85   *   &lt;/servlet&gt;
86   *   &lt;servlet-mapping&gt;
87   *     &lt;servlet-name&gt;template&lt;/servlet-name&gt;
88   *     &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
89   *   &lt;/servlet-mapping&gt;
90   * &lt;/web-app&gt;
91   * </code></pre>
92   * 
93   * <h4>Template engine configuration</h4>
94   * 
95   * <p>
96   * By default, the TemplateServer uses the {@link groovy.text.SimpleTemplateEngine}
97   * which interprets JSP-like templates. The init parameter <code>template.engine</code>
98   * defines the fully qualified class name of the template to use:
99   * <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 IOException 
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 }