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 }