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