001: /*
002: * $Id: TemplateServlet.java 4032 2006-08-30 07:18:49Z mguillem $
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: * <html>
070: * <body>
071: * <% 3.times { %>
072: * Hello World!
073: * <% } %>
074: * <br>
075: * </body>
076: * </html>
077: * </code></pre>
078: *
079: * Minimal <code>web.xml</code> example serving HTML-like templates
080: * <pre><code>
081: * <web-app>
082: * <servlet>
083: * <servlet-name>template</servlet-name>
084: * <servlet-class>groovy.servlet.TemplateServlet</servlet-class>
085: * </servlet>
086: * <servlet-mapping>
087: * <servlet-name>template</servlet-name>
088: * <url-pattern>*.html</url-pattern>
089: * </servlet-mapping>
090: * </web-app>
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,
142: boolean timestamp) {
143: if (file == null) {
144: throw new NullPointerException("file");
145: }
146: if (template == null) {
147: throw new NullPointerException("template");
148: }
149: if (timestamp) {
150: this .date = new Date(System.currentTimeMillis());
151: } else {
152: this .date = null;
153: }
154: this .hit = 0;
155: this .lastModified = file.lastModified();
156: this .length = file.length();
157: this .template = template;
158: }
159:
160: /**
161: * Checks the passed file attributes against those cached ones.
162: *
163: * @param file
164: * Other file handle to compare to the cached values.
165: * @return <code>true</code> if all measured values match, else <code>false</code>
166: */
167: public boolean validate(File file) {
168: if (file == null) {
169: throw new NullPointerException("file");
170: }
171: if (file.lastModified() != this .lastModified) {
172: return false;
173: }
174: if (file.length() != this .length) {
175: return false;
176: }
177: hit++;
178: return true;
179: }
180:
181: public String toString() {
182: if (date == null) {
183: return "Hit #" + hit;
184: }
185: return "Hit #" + hit + " since " + date;
186: }
187:
188: }
189:
190: /**
191: * Simple file name to template cache map.
192: */
193: private final Map cache;
194:
195: /**
196: * Underlying template engine used to evaluate template source files.
197: */
198: private TemplateEngine engine;
199:
200: /**
201: * Flag that controls the appending of the "Generated by ..." comment.
202: */
203: private boolean generateBy;
204:
205: /**
206: * Create new TemplateSerlvet.
207: */
208: public TemplateServlet() {
209: this .cache = new WeakHashMap();
210: this .engine = null; // assigned later by init()
211: this .generateBy = true; // may be changed by init()
212: }
213:
214: /**
215: * Gets the template created by the underlying engine parsing the request.
216: *
217: * <p>
218: * This method looks up a simple (weak) hash map for an existing template
219: * object that matches the source file. If the source file didn't change in
220: * length and its last modified stamp hasn't changed compared to a precompiled
221: * template object, this template is used. Otherwise, there is no or an
222: * invalid template object cache entry, a new one is created by the underlying
223: * template engine. This new instance is put to the cache for consecutive
224: * calls.
225: * </p>
226: *
227: * @return The template that will produce the response text.
228: * @param file
229: * The HttpServletRequest.
230: * @throws ServletException
231: * If the request specified an invalid template source file
232: */
233: protected Template getTemplate(File file) throws ServletException {
234:
235: String key = file.getAbsolutePath();
236: Template template = null;
237:
238: /*
239: * Test cache for a valid template bound to the key.
240: */
241: if (verbose) {
242: log("Looking for cached template by key \"" + key + "\"");
243: }
244: TemplateCacheEntry entry = (TemplateCacheEntry) cache.get(key);
245: if (entry != null) {
246: if (entry.validate(file)) {
247: if (verbose) {
248: log("Cache hit! " + entry);
249: }
250: template = entry.template;
251: } else {
252: if (verbose) {
253: log("Cached template needs recompiliation!");
254: }
255: }
256: } else {
257: if (verbose) {
258: log("Cache miss.");
259: }
260: }
261:
262: //
263: // Template not cached or the source file changed - compile new template!
264: //
265: if (template == null) {
266: if (verbose) {
267: log("Creating new template from file " + file + "...");
268: }
269: FileReader reader = null;
270: try {
271: reader = new FileReader(file);
272: template = engine.createTemplate(reader);
273: } catch (Exception e) {
274: throw new ServletException(
275: "Creation of template failed: " + e, e);
276: } finally {
277: if (reader != null) {
278: try {
279: reader.close();
280: } catch (IOException ignore) {
281: // e.printStackTrace();
282: }
283: }
284: }
285: cache.put(key, new TemplateCacheEntry(file, template,
286: verbose));
287: if (verbose) {
288: log("Created and added template to cache. [key=" + key
289: + "]");
290: }
291: }
292:
293: //
294: // Last sanity check.
295: //
296: if (template == null) {
297: throw new ServletException(
298: "Template is null? Should not happen here!");
299: }
300:
301: return template;
302:
303: }
304:
305: /**
306: * Initializes the servlet from hints the container passes.
307: * <p>
308: * Delegates to sub-init methods and parses the following parameters:
309: * <ul>
310: * <li> <tt>"generatedBy"</tt> : boolean, appends "Generated by ..." to the
311: * HTML response text generated by this servlet.
312: * </li>
313: * </ul>
314: * @param config
315: * Passed by the servlet container.
316: * @throws ServletException
317: * if this method encountered difficulties
318: *
319: * @see TemplateServlet#initTemplateEngine(ServletConfig)
320: */
321: public void init(ServletConfig config) throws ServletException {
322: super .init(config);
323: this .engine = initTemplateEngine(config);
324: if (engine == null) {
325: throw new ServletException(
326: "Template engine not instantiated.");
327: }
328: String value = config.getInitParameter("generated.by");
329: if (value != null) {
330: this .generateBy = Boolean.valueOf(value).booleanValue();
331: }
332: log("Servlet " + getClass().getName() + " initialized on "
333: + engine.getClass());
334: }
335:
336: /**
337: * Creates the template engine.
338: *
339: * Called by {@link TemplateServlet#init(ServletConfig)} and returns just
340: * <code>new groovy.text.SimpleTemplateEngine()</code> if the init parameter
341: * <code>template.engine</code> is not set by the container configuration.
342: *
343: * @param config
344: * Current serlvet configuration passed by the container.
345: *
346: * @return The underlying template engine or <code>null</code> on error.
347: */
348: protected TemplateEngine initTemplateEngine(ServletConfig config) {
349: String name = config.getInitParameter("template.engine");
350: if (name == null) {
351: return new SimpleTemplateEngine();
352: }
353: try {
354: return (TemplateEngine) Class.forName(name).newInstance();
355: } catch (InstantiationException e) {
356: log("Could not instantiate template engine: " + name, e);
357: } catch (IllegalAccessException e) {
358: log("Could not access template engine class: " + name, e);
359: } catch (ClassNotFoundException e) {
360: log("Could not find template engine class: " + name, e);
361: }
362: return null;
363: }
364:
365: /**
366: * Services the request with a response.
367: * <p>
368: * First the request is parsed for the source file uri. If the specified file
369: * could not be found or can not be read an error message is sent as response.
370: *
371: * </p>
372: * @param request
373: * The http request.
374: * @param response
375: * The http response.
376: * @throws IOException
377: * if an input or output error occurs while the servlet is
378: * handling the HTTP request
379: * @throws ServletException
380: * if the HTTP request cannot be handled
381: */
382: public void service(HttpServletRequest request,
383: HttpServletResponse response) throws ServletException,
384: IOException {
385:
386: if (verbose) {
387: log("Creating/getting cached template...");
388: }
389:
390: //
391: // Get the template source file handle.
392: //
393: File file = super .getScriptUriAsFile(request);
394: String name = file.getName();
395: if (!file.exists()) {
396: response.sendError(HttpServletResponse.SC_NOT_FOUND);
397: return; // throw new IOException(file.getAbsolutePath());
398: }
399: if (!file.canRead()) {
400: response.sendError(HttpServletResponse.SC_FORBIDDEN,
401: "Can not read \"" + name + "\"!");
402: return; // throw new IOException(file.getAbsolutePath());
403: }
404:
405: //
406: // Get the requested template.
407: //
408: long getMillis = System.currentTimeMillis();
409: Template template = getTemplate(file);
410: getMillis = System.currentTimeMillis() - getMillis;
411:
412: //
413: // Create new binding for the current request.
414: //
415: ServletBinding binding = new ServletBinding(request, response,
416: servletContext);
417: setVariables(binding);
418:
419: //
420: // Prepare the response buffer content type _before_ getting the writer.
421: // and set status code to ok
422: //
423: response.setContentType(CONTENT_TYPE_TEXT_HTML);
424: response.setStatus(HttpServletResponse.SC_OK);
425:
426: //
427: // Get the output stream writer from the binding.
428: //
429: Writer out = (Writer) binding.getVariable("out");
430: if (out == null) {
431: out = response.getWriter();
432: }
433:
434: //
435: // Evaluate the template.
436: //
437: if (verbose) {
438: log("Making template \"" + name + "\"...");
439: }
440: // String made = template.make(binding.getVariables()).toString();
441: // log(" = " + made);
442: long makeMillis = System.currentTimeMillis();
443: template.make(binding.getVariables()).writeTo(out);
444: makeMillis = System.currentTimeMillis() - makeMillis;
445:
446: if (generateBy) {
447: StringBuffer sb = new StringBuffer(100);
448: sb
449: .append("\n<!-- Generated by Groovy TemplateServlet [create/get=");
450: sb.append(Long.toString(getMillis));
451: sb.append(" ms, make=");
452: sb.append(Long.toString(makeMillis));
453: sb.append(" ms] -->\n");
454: out.write(sb.toString());
455: }
456:
457: //
458: // flush the response buffer.
459: //
460: response.flushBuffer();
461:
462: if (verbose) {
463: log("Template \"" + name
464: + "\" request responded. [create/get=" + getMillis
465: + " ms, make=" + makeMillis + " ms]");
466: }
467:
468: }
469:
470: /**
471: * Override this method to set your variables to the Groovy binding.
472: * <p>
473: * All variables bound the binding are passed to the template source text,
474: * e.g. the HTML file, when the template is merged.
475: * </p>
476: * <p>
477: * The binding provided by TemplateServlet does already include some default
478: * variables. As of this writing, they are (copied from
479: * {@link groovy.servlet.ServletBinding}):
480: * <ul>
481: * <li><tt>"request"</tt> : HttpServletRequest </li>
482: * <li><tt>"response"</tt> : HttpServletResponse </li>
483: * <li><tt>"context"</tt> : ServletContext </li>
484: * <li><tt>"application"</tt> : ServletContext </li>
485: * <li><tt>"session"</tt> : request.getSession(<b>false</b>) </li>
486: * </ul>
487: * </p>
488: * <p>
489: * And via implicite hard-coded keywords:
490: * <ul>
491: * <li><tt>"out"</tt> : response.getWriter() </li>
492: * <li><tt>"sout"</tt> : response.getOutputStream() </li>
493: * <li><tt>"html"</tt> : new MarkupBuilder(response.getWriter()) </li>
494: * </ul>
495: * </p>
496: *
497: * <p>Example binding all servlet context variables:
498: * <pre><code>
499: * class Mytlet extends TemplateServlet {
500: *
501: * protected void setVariables(ServletBinding binding) {
502: * // Bind a simple variable
503: * binding.setVariable("answer", new Long(42));
504: *
505: * // Bind all servlet context attributes...
506: * ServletContext context = (ServletContext) binding.getVariable("context");
507: * Enumeration enumeration = context.getAttributeNames();
508: * while (enumeration.hasMoreElements()) {
509: * String name = (String) enumeration.nextElement();
510: * binding.setVariable(name, context.getAttribute(name));
511: * }
512: * }
513: *
514: * }
515: * <code></pre>
516: * </p>
517: *
518: * @param binding
519: * to be modified
520: */
521: protected void setVariables(ServletBinding binding) {
522: // empty
523: }
524:
525: }
|