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.BufferedReader;
46 import java.io.File;
47 import java.io.FileReader;
48 import java.io.IOException;
49 import java.io.StringReader;
50 import java.io.Writer;
51 import java.util.Map;
52 import java.util.WeakHashMap;
53
54 import javax.servlet.ServletConfig;
55 import javax.servlet.ServletException;
56 import javax.servlet.http.HttpServletRequest;
57 import javax.servlet.http.HttpServletResponse;
58
59 /***
60 * A generic servlet for serving (mostly HTML) templates.
61 *
62 * It wraps a <code>groovy.text.TemplateEngine</code> to process HTTP
63 * requests. By default, it uses the
64 * <code>groovy.text.SimpleTemplateEngine</code> which interprets JSP-like (or
65 * Canvas-like) templates. The init parameter <code>templateEngine</code>
66 * defines the fully qualified class name of the template to use.<br>
67 *
68 * <p>
69 * Headless <code>helloworld.html</code> example
70 * <pre><code>
71 * <html>
72 * <body>
73 * <% 3.times { %>
74 * Hello World!
75 * <% } %>
76 * <br>
77 * session.id = ${session.id}
78 * </body>
79 * </html>
80 * </code></pre>
81 * </p>
82 *
83 * @see TemplateServlet#setVariables(ServletBinding)
84 *
85 * @author Christian Stein
86 * @author Guillaume Laforge
87 * @version 2.0
88 */
89 public class TemplateServlet extends AbstractHttpServlet {
90
91 /***
92 * Simple cache entry that validates against last modified and length
93 * attributes of the specified file.
94 *
95 * @author Sormuras
96 */
97 private static class TemplateCacheEntry {
98
99 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
139
140 private static final boolean VERBOSE = true;
141
142 /***
143 * Simple file name to template cache map.
144 */
145
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
163 this.cache = new WeakHashMap();
164
165 this.engine = null;
166 this.generatedBy = true;
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
188
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
230
231 TemplateCacheEntry entry = (TemplateCacheEntry) cache.get(key);
232 if (entry != null) {
233 if (entry.validate(file)) {
234 template = entry.template;
235 }
236 }
237
238
239
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
250
251
252
253
254
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
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
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
379
380 File file = super.getScriptUriAsFile(request);
381 if (!file.exists()) {
382 response.sendError(HttpServletResponse.SC_NOT_FOUND);
383 return;
384 }
385 if (!file.canRead()) {
386 response.sendError(HttpServletResponse.SC_FORBIDDEN, "Can not read!");
387 return;
388 }
389
390
391
392
393 long getMillis = System.currentTimeMillis();
394 Template template = getTemplate(file);
395 getMillis = System.currentTimeMillis() - getMillis;
396
397
398
399
400 ServletBinding binding = new ServletBinding(request, response, servletContext);
401 setVariables(binding);
402
403
404
405
406 response.setContentType(CONTENT_TYPE_TEXT_HTML);
407
408
409
410
411 Writer out = (Writer) binding.getVariable("out");
412 if (out == null) {
413 out = response.getWriter();
414 }
415
416
417
418
419 if (VERBOSE) {
420 log("Making template...");
421 }
422
423
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
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
507 }
508
509 }