1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 * <html>
70 * <body>
71 * <% 3.times { %>
72 * Hello World!
73 * <% } %>
74 * <br>
75 * </body>
76 * </html>
77 * </code></pre>
78 *
79 * Minimal <code>web.xml</code> example serving HTML-like templates
80 * <pre><code>
81 * <web-app>
82 * <servlet>
83 * <servlet-name>template</servlet-name>
84 * <servlet-class>groovy.servlet.TemplateServlet</servlet-class>
85 * </servlet>
86 * <servlet-mapping>
87 * <servlet-name>template</servlet-name>
88 * <url-pattern>*.html</url-pattern>
89 * </servlet-mapping>
90 * </web-app>
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);
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;
210 this.generateBy = true;
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
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
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
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
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
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;
389 }
390 if (!file.canRead()) {
391 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Can not read \"" + name + "\"!");
392 return;
393 }
394
395
396
397
398 long getMillis = System.currentTimeMillis();
399 Template template = getTemplate(file);
400 getMillis = System.currentTimeMillis() - getMillis;
401
402
403
404
405 ServletBinding binding = new ServletBinding(request, response, servletContext);
406 setVariables(binding);
407
408
409
410
411
412 response.setContentType(CONTENT_TYPE_TEXT_HTML);
413 response.setStatus(HttpServletResponse.SC_OK);
414
415
416
417
418 Writer out = (Writer) binding.getVariable("out");
419 if (out == null) {
420 out = response.getWriter();
421 }
422
423
424
425
426 if (verbose) {
427 log("Making template \"" + name + "\"...");
428 }
429
430
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
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
509 }
510
511 }