1 package org.apache.turbine.services.velocity;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.io.OutputStreamWriter;
23 import java.io.Writer;
24
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.Vector;
28
29 import javax.servlet.ServletConfig;
30
31 import org.apache.commons.collections.ExtendedProperties;
32 import org.apache.commons.configuration.Configuration;
33 import org.apache.commons.lang.StringUtils;
34 import org.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36
37 import org.apache.velocity.VelocityContext;
38 import org.apache.velocity.app.Velocity;
39 import org.apache.velocity.app.event.EventCartridge;
40 import org.apache.velocity.app.event.MethodExceptionEventHandler;
41 import org.apache.velocity.context.Context;
42 import org.apache.velocity.runtime.log.SimpleLog4JLogSystem;
43
44 import org.apache.turbine.Turbine;
45 import org.apache.turbine.services.InitializationException;
46 import org.apache.turbine.services.pull.PullService;
47 import org.apache.turbine.services.pull.TurbinePull;
48 import org.apache.turbine.services.template.BaseTemplateEngineService;
49 import org.apache.turbine.util.RunData;
50 import org.apache.turbine.util.TurbineException;
51
52 /***
53 * This is a Service that can process Velocity templates from within a
54 * Turbine Screen. It is used in conjunction with the templating service
55 * as a Templating Engine for templates ending in "vm". It registers
56 * itself as translation engine with the template service and gets
57 * accessed from there. After configuring it in your properties, it
58 * should never be necessary to call methods from this service directly.
59 *
60 * Here's an example of how you might use it from a
61 * screen:<br>
62 *
63 * <code>
64 * Context context = TurbineVelocity.getContext(data);<br>
65 * context.put("message", "Hello from Turbine!");<br>
66 * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
67 * data.getPage().getBody().addElement(results);<br>
68 * </code>
69 *
70 * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
71 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
72 * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
73 * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
74 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
75 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
76 * @version $Id: TurbineVelocityService.java 264152 2005-08-29 14:50:22Z henning $
77 */
78 public class TurbineVelocityService
79 extends BaseTemplateEngineService
80 implements VelocityService,
81 MethodExceptionEventHandler
82 {
83 /*** The generic resource loader path property in velocity.*/
84 private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
85
86 /*** Default character set to use if not specified in the RunData object. */
87 private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
88
89 /*** The prefix used for URIs which are of type <code>jar</code>. */
90 private static final String JAR_PREFIX = "jar:";
91
92 /*** The prefix used for URIs which are of type <code>absolute</code>. */
93 private static final String ABSOLUTE_PREFIX = "file://";
94
95 /*** Logging */
96 private static Log log = LogFactory.getLog(TurbineVelocityService.class);
97
98 /*** Is the pullModelActive? */
99 private boolean pullModelActive = false;
100
101 /*** Shall we catch Velocity Errors and report them in the log file? */
102 private boolean catchErrors = true;
103
104 /*** Internal Reference to the pull Service */
105 private PullService pullService = null;
106
107
108 /***
109 * Load all configured components and initialize them. This is
110 * a zero parameter variant which queries the Turbine Servlet
111 * for its config.
112 *
113 * @throws InitializationException Something went wrong in the init
114 * stage
115 */
116 public void init()
117 throws InitializationException
118 {
119 try
120 {
121 initVelocity();
122
123
124
125
126 if (TurbinePull.isRegistered())
127 {
128 pullModelActive = true;
129
130 pullService = TurbinePull.getService();
131
132 log.debug("Activated Pull Tools");
133 }
134
135
136 registerConfiguration(VelocityService.VELOCITY_EXTENSION);
137
138 setInit(true);
139 }
140 catch (Exception e)
141 {
142 throw new InitializationException(
143 "Failed to initialize TurbineVelocityService", e);
144 }
145 }
146
147
148 /***
149 * Inits the service using servlet parameters to obtain path to the
150 * configuration file.
151 *
152 * @param config The ServletConfiguration from Turbine
153 *
154 * @throws InitializationException Something went wrong when starting up.
155 * @deprecated use init() instead.
156 */
157 public void init(ServletConfig config)
158 throws InitializationException
159 {
160 init();
161 }
162
163
164 /***
165 * Create a Context object that also contains the globalContext.
166 *
167 * @return A Context object.
168 */
169 public Context getContext()
170 {
171 Context globalContext =
172 pullModelActive ? pullService.getGlobalContext() : null;
173
174 Context ctx = new VelocityContext(globalContext);
175 return ctx;
176 }
177
178 /***
179 * This method returns a new, empty Context object.
180 *
181 * @return A Context Object.
182 */
183 public Context getNewContext()
184 {
185 Context ctx = new VelocityContext();
186
187
188
189 EventCartridge ec = new EventCartridge();
190 ec.addEventHandler(this);
191 ec.attachToContext(ctx);
192 return ctx;
193 }
194
195 /***
196 * MethodException Event Cartridge handler
197 * for Velocity.
198 *
199 * It logs an execption thrown by the velocity processing
200 * on error level into the log file
201 *
202 * @param clazz The class that threw the exception
203 * @param method The Method name that threw the exception
204 * @param e The exception that would've been thrown
205 * @return A valid value to be used as Return value
206 * @throws Exception We threw the exception further up
207 */
208 public Object methodException(Class clazz, String method, Exception e)
209 throws Exception
210 {
211 log.error("Class " + clazz.getName() + "." + method + " threw Exception", e);
212
213 if (!catchErrors)
214 {
215 throw e;
216 }
217
218 return "[Turbine caught an Error here. Look into the turbine.log for further information]";
219 }
220
221 /***
222 * Create a Context from the RunData object. Adds a pointer to
223 * the RunData object to the VelocityContext so that RunData
224 * is available in the templates.
225 *
226 * @param data The Turbine RunData object.
227 * @return A clone of the WebContext needed by Velocity.
228 */
229 public Context getContext(RunData data)
230 {
231
232
233 Context context = (Context)
234 data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
235
236 if (context == null)
237 {
238 context = getContext();
239 context.put(VelocityService.RUNDATA_KEY, data);
240
241 if (pullModelActive)
242 {
243
244
245
246
247 pullService.populateContext(context, data);
248 }
249
250 data.getTemplateInfo().setTemplateContext(
251 VelocityService.CONTEXT, context);
252 }
253 return context;
254 }
255
256 /***
257 * Process the request and fill in the template with the values
258 * you set in the Context.
259 *
260 * @param context The populated context.
261 * @param filename The file name of the template.
262 * @return The process template as a String.
263 *
264 * @throws TurbineException Any exception trown while processing will be
265 * wrapped into a TurbineException and rethrown.
266 */
267 public String handleRequest(Context context, String filename)
268 throws TurbineException
269 {
270 String results = null;
271 ByteArrayOutputStream bytes = null;
272 OutputStreamWriter writer = null;
273 String charset = getCharSet(context);
274
275 try
276 {
277 bytes = new ByteArrayOutputStream();
278
279 writer = new OutputStreamWriter(bytes, charset);
280
281 executeRequest(context, filename, writer);
282 writer.flush();
283 results = bytes.toString(charset);
284 }
285 catch (Exception e)
286 {
287 renderingError(filename, e);
288 }
289 finally
290 {
291 try
292 {
293 if (bytes != null)
294 {
295 bytes.close();
296 }
297 }
298 catch (IOException ignored)
299 {
300
301 }
302 }
303 return results;
304 }
305
306 /***
307 * Process the request and fill in the template with the values
308 * you set in the Context.
309 *
310 * @param context A Context.
311 * @param filename A String with the filename of the template.
312 * @param output A OutputStream where we will write the process template as
313 * a String.
314 *
315 * @throws TurbineException Any exception trown while processing will be
316 * wrapped into a TurbineException and rethrown.
317 */
318 public void handleRequest(Context context, String filename,
319 OutputStream output)
320 throws TurbineException
321 {
322 String charset = getCharSet(context);
323 OutputStreamWriter writer = null;
324
325 try
326 {
327 writer = new OutputStreamWriter(output, charset);
328 executeRequest(context, filename, writer);
329 }
330 catch (Exception e)
331 {
332 renderingError(filename, e);
333 }
334 finally
335 {
336 try
337 {
338 if (writer != null)
339 {
340 writer.flush();
341 }
342 }
343 catch (Exception ignored)
344 {
345
346 }
347 }
348 }
349
350
351 /***
352 * Process the request and fill in the template with the values
353 * you set in the Context.
354 *
355 * @param context A Context.
356 * @param filename A String with the filename of the template.
357 * @param writer A Writer where we will write the process template as
358 * a String.
359 *
360 * @throws TurbineException Any exception trown while processing will be
361 * wrapped into a TurbineException and rethrown.
362 */
363 public void handleRequest(Context context, String filename, Writer writer)
364 throws TurbineException
365 {
366 try
367 {
368 executeRequest(context, filename, writer);
369 }
370 catch (Exception e)
371 {
372 renderingError(filename, e);
373 }
374 finally
375 {
376 try
377 {
378 if (writer != null)
379 {
380 writer.flush();
381 }
382 }
383 catch (Exception ignored)
384 {
385
386 }
387 }
388 }
389
390
391 /***
392 * Process the request and fill in the template with the values
393 * you set in the Context. Apply the character and template
394 * encodings from RunData to the result.
395 *
396 * @param context A Context.
397 * @param filename A String with the filename of the template.
398 * @param writer A OutputStream where we will write the process template as
399 * a String.
400 *
401 * @throws Exception A problem occured.
402 */
403 private void executeRequest(Context context, String filename,
404 Writer writer)
405 throws Exception
406 {
407 String encoding = getEncoding(context);
408
409 if (encoding != null)
410 {
411 Velocity.mergeTemplate(filename, encoding, context, writer);
412 }
413 else
414 {
415 Velocity.mergeTemplate(filename, context, writer);
416 }
417 }
418
419 /***
420 * Retrieve the required charset from the Turbine RunData in the context
421 *
422 * @param context A Context.
423 * @return The character set applied to the resulting String.
424 */
425 private String getCharSet(Context context)
426 {
427 String charset = null;
428
429 Object data = context.get(VelocityService.RUNDATA_KEY);
430 if ((data != null) && (data instanceof RunData))
431 {
432 charset = ((RunData) data).getCharSet();
433 }
434
435 return (StringUtils.isEmpty(charset)) ? DEFAULT_CHAR_SET : charset;
436 }
437
438 /***
439 * Retrieve the required encoding from the Turbine RunData in the context
440 *
441 * @param context A Context.
442 * @return The encoding applied to the resulting String.
443 */
444 private String getEncoding(Context context)
445 {
446 String encoding = null;
447
448 Object data = context.get(VelocityService.RUNDATA_KEY);
449 if ((data != null) && (data instanceof RunData))
450 {
451 encoding = ((RunData) data).getTemplateEncoding();
452 }
453
454 return encoding;
455 }
456
457 /***
458 * Macro to handle rendering errors.
459 *
460 * @param filename The file name of the unrenderable template.
461 * @param e The error.
462 *
463 * @exception TurbineException Thrown every time. Adds additional
464 * information to <code>e</code>.
465 */
466 private static void renderingError(String filename, Exception e)
467 throws TurbineException
468 {
469 String err = "Error rendering Velocity template: " + filename;
470 log.error(err, e);
471 throw new TurbineException(err, e);
472 }
473
474 /***
475 * Setup the velocity runtime by using a subset of the
476 * Turbine configuration which relates to velocity.
477 *
478 * @exception Exception An Error occured.
479 */
480 private synchronized void initVelocity()
481 throws Exception
482 {
483
484 Configuration conf = getConfiguration();
485
486 catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
487
488 conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
489 SimpleLog4JLogSystem.class.getName());
490 conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM
491 + ".log4j.category", "velocity");
492
493 Velocity.setExtendedProperties(createVelocityProperties(conf));
494 Velocity.init();
495 }
496
497
498 /***
499 * This method generates the Extended Properties object necessary
500 * for the initialization of Velocity. It also converts the various
501 * resource loader pathes into webapp relative pathes. It also
502 *
503 * @param conf The Velocity Service configuration
504 *
505 * @return An ExtendedProperties Object for Velocity
506 *
507 * @throws Exception If a problem occured while converting the properties.
508 */
509
510 public ExtendedProperties createVelocityProperties(Configuration conf)
511 throws Exception
512 {
513
514
515
516 ExtendedProperties veloConfig = new ExtendedProperties();
517
518
519
520
521
522 for (Iterator i = conf.getKeys(); i.hasNext();)
523 {
524 String key = (String) i.next();
525 if (!key.endsWith(RESOURCE_LOADER_PATH))
526 {
527 Object value = conf.getProperty(key);
528
529
530
531
532
533
534
535 if (value instanceof List)
536 {
537 List srcValue = (List) value;
538 Vector targetValue = new Vector(srcValue.size());
539
540 for (Iterator it = srcValue.iterator(); it.hasNext(); )
541 {
542 targetValue.add(it.next());
543 }
544
545 veloConfig.addProperty(key, targetValue);
546 }
547 else
548 {
549 veloConfig.addProperty(key, value);
550 }
551
552 continue;
553 }
554
555 List paths = conf.getList(key, null);
556 if (paths == null)
557 {
558
559
560 continue;
561 }
562
563 Velocity.clearProperty(key);
564
565
566
567
568
569
570
571
572
573 for (Iterator j = paths.iterator(); j.hasNext();)
574 {
575 String path = (String) j.next();
576
577 log.debug("Translating " + path);
578
579 if (path.startsWith(JAR_PREFIX))
580 {
581
582 if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
583 {
584
585 int jarSepIndex = path.indexOf("!/");
586
587
588 path = (jarSepIndex < 0)
589 ? Turbine.getRealPath(path.substring(11))
590
591 : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
592
593 log.debug("Result (absolute jar path): " + path);
594 }
595 }
596 else if (path.startsWith(ABSOLUTE_PREFIX))
597 {
598
599 path = Turbine.getRealPath(path.substring(7));
600
601 log.debug("Result (absolute URL Path): " + path);
602 }
603
604 else if (path.indexOf("://") < 0)
605 {
606 path = Turbine.getRealPath(path);
607
608 log.debug("Result (normal fs reference): " + path);
609 }
610
611 log.debug("Adding " + key + " -> " + path);
612
613 veloConfig.addProperty(key, path);
614 }
615 }
616 return veloConfig;
617 }
618
619 /***
620 * Find out if a given template exists. Velocity
621 * will do its own searching to determine whether
622 * a template exists or not.
623 *
624 * @param template String template to search for
625 * @return True if the template can be loaded by Velocity
626 */
627 public boolean templateExists(String template)
628 {
629 return Velocity.templateExists(template);
630 }
631
632 /***
633 * Performs post-request actions (releases context
634 * tools back to the object pool).
635 *
636 * @param context a Velocity Context
637 */
638 public void requestFinished(Context context)
639 {
640 if (pullModelActive)
641 {
642 pullService.releaseTools(context);
643 }
644 }
645 }