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