View Javadoc

1   /*
2    * $Id: TemplateServlet.java,v 1.16 2005/06/05 08:16:07 cstein 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.BufferedReader;
46  import java.io.File;
47  import java.io.FileReader;
48  import java.io.IOException;
49  import java.io.StringReader;
50  import java.io.Writer;
51  import java.util.Map;
52  import java.util.WeakHashMap;
53  
54  import javax.servlet.ServletConfig;
55  import javax.servlet.ServletException;
56  import javax.servlet.http.HttpServletRequest;
57  import javax.servlet.http.HttpServletResponse;
58  
59  /***
60   * A generic servlet for serving (mostly HTML) templates.
61   * 
62   * It wraps a <code>groovy.text.TemplateEngine</code> to process HTTP
63   * requests. By default, it uses the
64   * <code>groovy.text.SimpleTemplateEngine</code> which interprets JSP-like (or
65   * Canvas-like) templates. The init parameter <code>templateEngine</code>
66   * defines the fully qualified class name of the template to use.<br>
67   * 
68   * <p>
69   * Headless <code>helloworld.html</code> example
70   * <pre><code>
71   *  &lt;html&gt;
72   *    &lt;body&gt;
73   *      &lt;% 3.times { %&gt;
74   *        Hello World!
75   *      &lt;% } %&gt;
76   *      &lt;br&gt;
77   *      session.id = ${session.id}
78   *    &lt;/body&gt;
79   *  &lt;/html&gt; 
80   * </code></pre>
81   * </p>
82   * 
83   * @see TemplateServlet#setVariables(ServletBinding)
84   * 
85   * @author Christian Stein
86   * @author Guillaume Laforge
87   * @version 2.0
88   */
89  public class TemplateServlet extends AbstractHttpServlet {
90  
91    /***
92     * Simple cache entry that validates against last modified and length
93     * attributes of the specified file. 
94     *
95     * @author Sormuras
96     */
97    private static class TemplateCacheEntry {
98  
99      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 }