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 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Vector;
30
31 import javax.servlet.ServletConfig;
32
33 import org.apache.commons.collections.ExtendedProperties;
34 import org.apache.commons.configuration.Configuration;
35 import org.apache.commons.lang.StringUtils;
36 import org.apache.commons.logging.Log;
37 import org.apache.commons.logging.LogFactory;
38 import org.apache.turbine.Turbine;
39 import org.apache.turbine.services.InitializationException;
40 import org.apache.turbine.services.pull.PullService;
41 import org.apache.turbine.services.pull.TurbinePull;
42 import org.apache.turbine.services.template.BaseTemplateEngineService;
43 import org.apache.turbine.util.RunData;
44 import org.apache.turbine.util.TurbineException;
45 import org.apache.velocity.VelocityContext;
46 import org.apache.velocity.app.Velocity;
47 import org.apache.velocity.app.event.EventCartridge;
48 import org.apache.velocity.app.event.MethodExceptionEventHandler;
49 import org.apache.velocity.context.Context;
50 import org.apache.velocity.runtime.log.Log4JLogChute;
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 552336 2007-07-01 16:33:47Z tv $
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 /*** Default encoding to use if not specified in the RunData object. */
105 private String defaultEncoding = DEFAULT_CHAR_SET;
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 Velocity.mergeTemplate(filename, encoding, context, writer);
413 }
414
415 /***
416 * Retrieve the required charset from the Turbine RunData in the context
417 *
418 * @param context A Context.
419 * @return The character set applied to the resulting String.
420 */
421 private String getCharSet(Context context)
422 {
423 String charset = null;
424
425 Object data = context.get(VelocityService.RUNDATA_KEY);
426 if ((data != null) && (data instanceof RunData))
427 {
428 charset = ((RunData) data).getCharSet();
429 }
430
431 return (StringUtils.isEmpty(charset)) ? DEFAULT_CHAR_SET : charset;
432 }
433
434 /***
435 * Retrieve the required encoding from the Turbine RunData in the context
436 *
437 * @param context A Context.
438 * @return The encoding applied to the resulting String.
439 */
440 private String getEncoding(Context context)
441 {
442 String encoding = null;
443
444 Object data = context.get(VelocityService.RUNDATA_KEY);
445 if ((data != null) && (data instanceof RunData))
446 {
447 encoding = ((RunData) data).getTemplateEncoding();
448 }
449
450 return (StringUtils.isEmpty(encoding)) ? defaultEncoding : encoding;
451 }
452
453 /***
454 * Macro to handle rendering errors.
455 *
456 * @param filename The file name of the unrenderable template.
457 * @param e The error.
458 *
459 * @exception TurbineException Thrown every time. Adds additional
460 * information to <code>e</code>.
461 */
462 private static void renderingError(String filename, Exception e)
463 throws TurbineException
464 {
465 String err = "Error rendering Velocity template: " + filename;
466 log.error(err, e);
467 throw new TurbineException(err, e);
468 }
469
470 /***
471 * Setup the velocity runtime by using a subset of the
472 * Turbine configuration which relates to velocity.
473 *
474 * @exception Exception An Error occured.
475 */
476 private synchronized void initVelocity()
477 throws Exception
478 {
479
480 Configuration conf = getConfiguration();
481
482 catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
483
484 if (conf.containsKey(Velocity.INPUT_ENCODING))
485 {
486 defaultEncoding = conf.getString(Velocity.INPUT_ENCODING);
487 }
488
489 conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
490 Log4JLogChute.class.getName());
491 conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM
492 + ".log4j.category", "velocity");
493
494 Velocity.setExtendedProperties(createVelocityProperties(conf));
495 Velocity.init();
496 }
497
498
499 /***
500 * This method generates the Extended Properties object necessary
501 * for the initialization of Velocity. It also converts the various
502 * resource loader pathes into webapp relative pathes. It also
503 *
504 * @param conf The Velocity Service configuration
505 *
506 * @return An ExtendedProperties Object for Velocity
507 *
508 * @throws Exception If a problem occured while converting the properties.
509 */
510
511 public ExtendedProperties createVelocityProperties(Configuration conf)
512 throws Exception
513 {
514
515
516
517 ExtendedProperties veloConfig = new ExtendedProperties();
518
519
520
521
522
523 for (Iterator i = conf.getKeys(); i.hasNext();)
524 {
525 String key = (String) i.next();
526 if (!key.endsWith(RESOURCE_LOADER_PATH))
527 {
528 Object value = conf.getProperty(key);
529
530
531
532
533
534
535
536 if (value instanceof List)
537 {
538 List srcValue = (List) value;
539 Vector targetValue = new Vector(srcValue.size());
540
541 for (Iterator it = srcValue.iterator(); it.hasNext(); )
542 {
543 targetValue.add(it.next());
544 }
545
546 veloConfig.addProperty(key, targetValue);
547 }
548 else
549 {
550 veloConfig.addProperty(key, value);
551 }
552
553 continue;
554 }
555
556 List paths = conf.getList(key, null);
557 if (paths == null)
558 {
559
560
561 continue;
562 }
563
564 Velocity.clearProperty(key);
565
566
567
568
569
570
571
572
573
574 for (Iterator j = paths.iterator(); j.hasNext();)
575 {
576 String path = (String) j.next();
577
578 log.debug("Translating " + path);
579
580 if (path.startsWith(JAR_PREFIX))
581 {
582
583 if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
584 {
585
586 int jarSepIndex = path.indexOf("!/");
587
588
589 path = (jarSepIndex < 0)
590 ? Turbine.getRealPath(path.substring(11))
591
592 : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
593
594 log.debug("Result (absolute jar path): " + path);
595 }
596 }
597 else if (path.startsWith(ABSOLUTE_PREFIX))
598 {
599
600 path = Turbine.getRealPath(path.substring(7));
601
602 log.debug("Result (absolute URL Path): " + path);
603 }
604
605 else if (path.indexOf("://") < 0)
606 {
607 path = Turbine.getRealPath(path);
608
609 log.debug("Result (normal fs reference): " + path);
610 }
611
612 log.debug("Adding " + key + " -> " + path);
613
614 veloConfig.addProperty(key, path);
615 }
616 }
617 return veloConfig;
618 }
619
620 /***
621 * Find out if a given template exists. Velocity
622 * will do its own searching to determine whether
623 * a template exists or not.
624 *
625 * @param template String template to search for
626 * @return True if the template can be loaded by Velocity
627 */
628 public boolean templateExists(String template)
629 {
630 return Velocity.resourceExists(template);
631 }
632
633 /***
634 * Performs post-request actions (releases context
635 * tools back to the object pool).
636 *
637 * @param context a Velocity Context
638 */
639 public void requestFinished(Context context)
640 {
641 if (pullModelActive)
642 {
643 pullService.releaseTools(context);
644 }
645 }
646 }