001    /*
002     * $Id: TemplateServlet.java,v 1.20 2005/07/29 11:36:17 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.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     *  &lt;html&gt;
070     *    &lt;body&gt;
071     *      &lt;% 3.times { %&gt;
072     *        Hello World!
073     *      &lt;% } %&gt;
074     *      &lt;br&gt;
075     *    &lt;/body&gt;
076     *  &lt;/html&gt; 
077     * </code></pre>
078     * 
079     * Minimal <code>web.xml</code> example serving HTML-like templates
080     * <pre><code>
081     * &lt;web-app&gt;
082     *   &lt;servlet&gt;
083     *     &lt;servlet-name&gt;template&lt;/servlet-name&gt;
084     *     &lt;servlet-class&gt;groovy.servlet.TemplateServlet&lt;/servlet-class&gt;
085     *   &lt;/servlet&gt;
086     *   &lt;servlet-mapping&gt;
087     *     &lt;servlet-name&gt;template&lt;/servlet-name&gt;
088     *     &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
089     *   &lt;/servlet-mapping&gt;
090     * &lt;/web-app&gt;
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 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            //
411            response.setContentType(CONTENT_TYPE_TEXT_HTML);
412    
413            //
414            // Get the output stream writer from the binding.
415            //
416            Writer out = (Writer) binding.getVariable("out");
417            if (out == null) {
418                out = response.getWriter();
419            }
420    
421            //
422            // Evaluate the template.
423            //
424            if (verbose) {
425                log("Making template \"" + name + "\"...");
426            }
427            // String made = template.make(binding.getVariables()).toString();
428            // log(" = " + made);
429            long makeMillis = System.currentTimeMillis();
430            template.make(binding.getVariables()).writeTo(out);
431            makeMillis = System.currentTimeMillis() - makeMillis;
432    
433            if (generateBy) {
434                StringBuffer sb = new StringBuffer(100);
435                sb.append("\n<!-- Generated by Groovy TemplateServlet [create/get=");
436                sb.append(Long.toString(getMillis));
437                sb.append(" ms, make=");
438                sb.append(Long.toString(makeMillis));
439                sb.append(" ms] -->\n");
440                out.write(sb.toString());
441            }
442    
443            //
444            // Set status code and flush the response buffer.
445            //
446            response.setStatus(HttpServletResponse.SC_OK);
447            response.flushBuffer();
448    
449            if (verbose) {
450                log("Template \"" + name + "\" request responded. [create/get=" + getMillis + " ms, make=" + makeMillis + " ms]");
451            }
452    
453        }
454    
455        /**
456         * Override this method to set your variables to the Groovy binding.
457         * <p>
458         * All variables bound the binding are passed to the template source text, 
459         * e.g. the HTML file, when the template is merged.
460         * </p>
461         * <p>
462         * The binding provided by TemplateServlet does already include some default
463         * variables. As of this writing, they are (copied from 
464         * {@link groovy.servlet.ServletBinding}):
465         * <ul>
466         * <li><tt>"request"</tt> : HttpServletRequest </li>
467         * <li><tt>"response"</tt> : HttpServletResponse </li>
468         * <li><tt>"context"</tt> : ServletContext </li>
469         * <li><tt>"application"</tt> : ServletContext </li>
470         * <li><tt>"session"</tt> : request.getSession(<b>false</b>) </li>
471         * </ul>
472         * </p>
473         * <p>
474         * And via implicite hard-coded keywords:
475         * <ul>
476         * <li><tt>"out"</tt> : response.getWriter() </li>
477         * <li><tt>"sout"</tt> : response.getOutputStream() </li>
478         * <li><tt>"html"</tt> : new MarkupBuilder(response.getWriter()) </li>
479         * </ul>
480         * </p>
481         *
482         * <p>Example binding all servlet context variables:
483         * <pre><code>
484         * class Mytlet extends TemplateServlet {
485         * 
486         *   protected void setVariables(ServletBinding binding) {
487         *     // Bind a simple variable
488         *     binding.setVariable("answer", new Long(42));
489         *   
490         *     // Bind all servlet context attributes...
491         *     ServletContext context = (ServletContext) binding.getVariable("context");
492         *     Enumeration enumeration = context.getAttributeNames();
493         *     while (enumeration.hasMoreElements()) {
494         *       String name = (String) enumeration.nextElement();
495         *       binding.setVariable(name, context.getAttribute(name));
496         *     }
497         *   }
498         * 
499         * }
500         * <code></pre>
501         * </p>
502         * 
503         * @param binding
504         *  to be modified
505         */
506        protected void setVariables(ServletBinding binding) {
507            // empty
508        }
509    
510    }