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    }