001 package org.apache.turbine.services.velocity;
002
003
004 /*
005 * Licensed to the Apache Software Foundation (ASF) under one
006 * or more contributor license agreements. See the NOTICE file
007 * distributed with this work for additional information
008 * regarding copyright ownership. The ASF licenses this file
009 * to you under the Apache License, Version 2.0 (the
010 * "License"); you may not use this file except in compliance
011 * with the License. You may obtain a copy of the License at
012 *
013 * http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing,
016 * software distributed under the License is distributed on an
017 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
018 * KIND, either express or implied. See the License for the
019 * specific language governing permissions and limitations
020 * under the License.
021 */
022
023
024 import java.io.ByteArrayOutputStream;
025 import java.io.IOException;
026 import java.io.OutputStream;
027 import java.io.OutputStreamWriter;
028 import java.io.Writer;
029 import java.util.Iterator;
030 import java.util.List;
031
032 import org.apache.commons.collections.ExtendedProperties;
033 import org.apache.commons.configuration.Configuration;
034 import org.apache.commons.lang.StringUtils;
035 import org.apache.commons.logging.Log;
036 import org.apache.commons.logging.LogFactory;
037 import org.apache.turbine.Turbine;
038 import org.apache.turbine.pipeline.PipelineData;
039 import org.apache.turbine.services.InitializationException;
040 import org.apache.turbine.services.pull.PullService;
041 import org.apache.turbine.services.pull.TurbinePull;
042 import org.apache.turbine.services.template.BaseTemplateEngineService;
043 import org.apache.turbine.util.RunData;
044 import org.apache.turbine.util.TurbineException;
045 import org.apache.velocity.VelocityContext;
046 import org.apache.velocity.app.Velocity;
047 import org.apache.velocity.app.event.EventCartridge;
048 import org.apache.velocity.app.event.MethodExceptionEventHandler;
049 import org.apache.velocity.context.Context;
050 import org.apache.velocity.runtime.log.Log4JLogChute;
051
052 /**
053 * This is a Service that can process Velocity templates from within a
054 * Turbine Screen. It is used in conjunction with the templating service
055 * as a Templating Engine for templates ending in "vm". It registers
056 * itself as translation engine with the template service and gets
057 * accessed from there. After configuring it in your properties, it
058 * should never be necessary to call methods from this service directly.
059 *
060 * Here's an example of how you might use it from a
061 * screen:<br>
062 *
063 * <code>
064 * Context context = TurbineVelocity.getContext(data);<br>
065 * context.put("message", "Hello from Turbine!");<br>
066 * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
067 * data.getPage().getBody().addElement(results);<br>
068 * </code>
069 *
070 * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
071 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
072 * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
073 * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
074 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
075 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
076 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
077 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
078 * @version $Id: TurbineVelocityService.java 1073172 2011-02-21 22:16:51Z tv $
079 */
080 public class TurbineVelocityService
081 extends BaseTemplateEngineService
082 implements VelocityService,
083 MethodExceptionEventHandler
084 {
085 /** The generic resource loader path property in velocity.*/
086 private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
087
088 /** Default character set to use if not specified in the RunData object. */
089 private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
090
091 /** The prefix used for URIs which are of type <code>jar</code>. */
092 private static final String JAR_PREFIX = "jar:";
093
094 /** The prefix used for URIs which are of type <code>absolute</code>. */
095 private static final String ABSOLUTE_PREFIX = "file://";
096
097 /** Logging */
098 private static Log log = LogFactory.getLog(TurbineVelocityService.class);
099
100 /** Is the pullModelActive? */
101 private boolean pullModelActive = false;
102
103 /** Shall we catch Velocity Errors and report them in the log file? */
104 private boolean catchErrors = true;
105
106 /** Internal Reference to the pull Service */
107 private PullService pullService = null;
108
109
110 /**
111 * Load all configured components and initialize them. This is
112 * a zero parameter variant which queries the Turbine Servlet
113 * for its config.
114 *
115 * @throws InitializationException Something went wrong in the init
116 * stage
117 */
118 @Override
119 public void init()
120 throws InitializationException
121 {
122 try
123 {
124 initVelocity();
125
126 // We can only load the Pull Model ToolBox
127 // if the Pull service has been listed in the TR.props
128 // and the service has successfully been initialized.
129 if (TurbinePull.isRegistered())
130 {
131 pullModelActive = true;
132
133 pullService = TurbinePull.getService();
134
135 log.debug("Activated Pull Tools");
136 }
137
138 // Register with the template service.
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 * Create a Context object that also contains the globalContext.
152 *
153 * @return A Context object.
154 */
155 public Context getContext()
156 {
157 Context globalContext =
158 pullModelActive ? pullService.getGlobalContext() : null;
159
160 Context ctx = new VelocityContext(globalContext);
161 return ctx;
162 }
163
164 /**
165 * This method returns a new, empty Context object.
166 *
167 * @return A Context Object.
168 */
169 public Context getNewContext()
170 {
171 Context ctx = new VelocityContext();
172
173 // Attach an Event Cartridge to it, so we get exceptions
174 // while invoking methods from the Velocity Screens
175 EventCartridge ec = new EventCartridge();
176 ec.addEventHandler(this);
177 ec.attachToContext(ctx);
178 return ctx;
179 }
180
181 /**
182 * MethodException Event Cartridge handler
183 * for Velocity.
184 *
185 * It logs an execption thrown by the velocity processing
186 * on error level into the log file
187 *
188 * @param clazz The class that threw the exception
189 * @param method The Method name that threw the exception
190 * @param e The exception that would've been thrown
191 * @return A valid value to be used as Return value
192 * @throws Exception We threw the exception further up
193 */
194 public Object methodException(Class clazz, String method, Exception e)
195 throws Exception
196 {
197 log.error("Class " + clazz.getName() + "." + method + " threw Exception", e);
198
199 if (!catchErrors)
200 {
201 throw e;
202 }
203
204 return "[Turbine caught an Error here. Look into the turbine.log for further information]";
205 }
206
207 /**
208 * Create a Context from the RunData object. Adds a pointer to
209 * the RunData object to the VelocityContext so that RunData
210 * is available in the templates.
211 * @deprecated. Use PipelineData version.
212 * @param data The Turbine RunData object.
213 * @return A clone of the WebContext needed by Velocity.
214 */
215 public Context getContext(RunData data)
216 {
217 // Attempt to get it from the data first. If it doesn't
218 // exist, create it and then stuff it into the data.
219 Context context = (Context)
220 data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
221
222 if (context == null)
223 {
224 context = getContext();
225 context.put(VelocityService.RUNDATA_KEY, data);
226
227 if (pullModelActive)
228 {
229 // Populate the toolbox with request scope, session scope
230 // and persistent scope tools (global tools are already in
231 // the toolBoxContent which has been wrapped to construct
232 // this request-specific context).
233 pullService.populateContext(context, data);
234 }
235
236 data.getTemplateInfo().setTemplateContext(
237 VelocityService.CONTEXT, context);
238 }
239 return context;
240 }
241
242 /**
243 * Create a Context from the PipelineData object. Adds a pointer to
244 * the RunData object to the VelocityContext so that RunData
245 * is available in the templates.
246 *
247 * @param data The Turbine RunData object.
248 * @return A clone of the WebContext needed by Velocity.
249 */
250 public Context getContext(PipelineData pipelineData)
251 {
252 //Map runDataMap = (Map)pipelineData.get(RunData.class);
253 RunData data = (RunData)pipelineData;
254 // Attempt to get it from the data first. If it doesn't
255 // exist, create it and then stuff it into the data.
256 Context context = (Context)
257 data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
258
259 if (context == null)
260 {
261 context = getContext();
262 context.put(VelocityService.RUNDATA_KEY, data);
263 // we will add both data and pipelineData to the context.
264 context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
265
266 if (pullModelActive)
267 {
268 // Populate the toolbox with request scope, session scope
269 // and persistent scope tools (global tools are already in
270 // the toolBoxContent which has been wrapped to construct
271 // this request-specific context).
272 pullService.populateContext(context, pipelineData);
273 }
274
275 data.getTemplateInfo().setTemplateContext(
276 VelocityService.CONTEXT, context);
277 }
278 return context;
279 }
280
281 /**
282 * Process the request and fill in the template with the values
283 * you set in the Context.
284 *
285 * @param context The populated context.
286 * @param filename The file name of the template.
287 * @return The process template as a String.
288 *
289 * @throws TurbineException Any exception trown while processing will be
290 * wrapped into a TurbineException and rethrown.
291 */
292 public String handleRequest(Context context, String filename)
293 throws TurbineException
294 {
295 String results = null;
296 ByteArrayOutputStream bytes = null;
297 OutputStreamWriter writer = null;
298 String charset = getCharSet(context);
299
300 try
301 {
302 bytes = new ByteArrayOutputStream();
303
304 writer = new OutputStreamWriter(bytes, charset);
305
306 executeRequest(context, filename, writer);
307 writer.flush();
308 results = bytes.toString(charset);
309 }
310 catch (Exception e)
311 {
312 renderingError(filename, e);
313 }
314 finally
315 {
316 try
317 {
318 if (bytes != null)
319 {
320 bytes.close();
321 }
322 }
323 catch (IOException ignored)
324 {
325 // do nothing.
326 }
327 }
328 return results;
329 }
330
331 /**
332 * Process the request and fill in the template with the values
333 * you set in the Context.
334 *
335 * @param context A Context.
336 * @param filename A String with the filename of the template.
337 * @param output A OutputStream where we will write the process template as
338 * a String.
339 *
340 * @throws TurbineException Any exception trown while processing will be
341 * wrapped into a TurbineException and rethrown.
342 */
343 public void handleRequest(Context context, String filename,
344 OutputStream output)
345 throws TurbineException
346 {
347 String charset = getCharSet(context);
348 OutputStreamWriter writer = null;
349
350 try
351 {
352 writer = new OutputStreamWriter(output, charset);
353 executeRequest(context, filename, writer);
354 }
355 catch (Exception e)
356 {
357 renderingError(filename, e);
358 }
359 finally
360 {
361 try
362 {
363 if (writer != null)
364 {
365 writer.flush();
366 }
367 }
368 catch (Exception ignored)
369 {
370 // do nothing.
371 }
372 }
373 }
374
375
376 /**
377 * Process the request and fill in the template with the values
378 * you set in the Context.
379 *
380 * @param context A Context.
381 * @param filename A String with the filename of the template.
382 * @param writer A Writer where we will write the process template as
383 * a String.
384 *
385 * @throws TurbineException Any exception trown while processing will be
386 * wrapped into a TurbineException and rethrown.
387 */
388 public void handleRequest(Context context, String filename, Writer writer)
389 throws TurbineException
390 {
391 try
392 {
393 executeRequest(context, filename, writer);
394 }
395 catch (Exception e)
396 {
397 renderingError(filename, e);
398 }
399 finally
400 {
401 try
402 {
403 if (writer != null)
404 {
405 writer.flush();
406 }
407 }
408 catch (Exception ignored)
409 {
410 // do nothing.
411 }
412 }
413 }
414
415
416 /**
417 * Process the request and fill in the template with the values
418 * you set in the Context. Apply the character and template
419 * encodings from RunData to the result.
420 *
421 * @param context A Context.
422 * @param filename A String with the filename of the template.
423 * @param writer A OutputStream where we will write the process template as
424 * a String.
425 *
426 * @throws Exception A problem occured.
427 */
428 private void executeRequest(Context context, String filename,
429 Writer writer)
430 throws Exception
431 {
432 String encoding = getEncoding(context);
433
434 if (encoding == null)
435 {
436 encoding = DEFAULT_CHAR_SET;
437 }
438 Velocity.mergeTemplate(filename, encoding, context, writer);
439 }
440
441 /**
442 * Retrieve the required charset from the Turbine RunData in the context
443 *
444 * @param context A Context.
445 * @return The character set applied to the resulting String.
446 */
447 private String getCharSet(Context context)
448 {
449 String charset = null;
450
451 Object data = context.get(VelocityService.RUNDATA_KEY);
452 if ((data != null) && (data instanceof RunData))
453 {
454 charset = ((RunData) data).getCharSet();
455 }
456
457 return (StringUtils.isEmpty(charset)) ? DEFAULT_CHAR_SET : charset;
458 }
459
460 /**
461 * Retrieve the required encoding from the Turbine RunData in the context
462 *
463 * @param context A Context.
464 * @return The encoding applied to the resulting String.
465 */
466 private String getEncoding(Context context)
467 {
468 String encoding = null;
469
470 Object data = context.get(VelocityService.RUNDATA_KEY);
471 if ((data != null) && (data instanceof RunData))
472 {
473 encoding = ((RunData) data).getTemplateEncoding();
474 }
475
476 return encoding;
477 }
478
479 /**
480 * Macro to handle rendering errors.
481 *
482 * @param filename The file name of the unrenderable template.
483 * @param e The error.
484 *
485 * @exception TurbineException Thrown every time. Adds additional
486 * information to <code>e</code>.
487 */
488 private static final void renderingError(String filename, Exception e)
489 throws TurbineException
490 {
491 String err = "Error rendering Velocity template: " + filename;
492 log.error(err, e);
493 throw new TurbineException(err, e);
494 }
495
496 /**
497 * Setup the velocity runtime by using a subset of the
498 * Turbine configuration which relates to velocity.
499 *
500 * @exception Exception An Error occured.
501 */
502 private synchronized void initVelocity()
503 throws Exception
504 {
505 // Get the configuration for this service.
506 Configuration conf = getConfiguration();
507
508 catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
509
510 conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
511 Log4JLogChute.class.getName());
512 conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM
513 + ".log4j.category", "velocity");
514
515 Velocity.setExtendedProperties(createVelocityProperties(conf));
516 Velocity.init();
517 }
518
519
520 /**
521 * This method generates the Extended Properties object necessary
522 * for the initialization of Velocity. It also converts the various
523 * resource loader pathes into webapp relative pathes. It also
524 *
525 * @param conf The Velocity Service configuration
526 *
527 * @return An ExtendedProperties Object for Velocity
528 *
529 * @throws Exception If a problem occured while converting the properties.
530 */
531
532 public ExtendedProperties createVelocityProperties(Configuration conf)
533 throws Exception
534 {
535 // This bugger is public, because we want to run some Unit tests
536 // on it.
537
538 ExtendedProperties veloConfig = new ExtendedProperties();
539
540 // Fix up all the template resource loader pathes to be
541 // webapp relative. Copy all other keys verbatim into the
542 // veloConfiguration.
543
544 for (Iterator i = conf.getKeys(); i.hasNext();)
545 {
546 String key = (String) i.next();
547 if (!key.endsWith(RESOURCE_LOADER_PATH))
548 {
549 Object value = conf.getProperty(key);
550 if (value instanceof List) {
551 for (Iterator itr = ((List)value).iterator(); itr.hasNext();)
552 {
553 veloConfig.addProperty(key, itr.next());
554 }
555 }
556 else
557 {
558 veloConfig.addProperty(key, value);
559 }
560 continue; // for()
561 }
562
563 List paths = conf.getList(key, null);
564 if (paths == null)
565 {
566 // We don't copy this into VeloProperties, because
567 // null value is unhealthy for the ExtendedProperties object...
568 continue; // for()
569 }
570
571 Velocity.clearProperty(key);
572
573 // Translate the supplied pathes given here.
574 // the following three different kinds of
575 // pathes must be translated to be webapp-relative
576 //
577 // jar:file://path-component!/entry-component
578 // file://path-component
579 // path/component
580 for (Iterator j = paths.iterator(); j.hasNext();)
581 {
582 String path = (String) j.next();
583
584 log.debug("Translating " + path);
585
586 if (path.startsWith(JAR_PREFIX))
587 {
588 // skip jar: -> 4 chars
589 if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
590 {
591 // We must convert up to the jar path separator
592 int jarSepIndex = path.indexOf("!/");
593
594 // jar:file:// -> skip 11 chars
595 path = (jarSepIndex < 0)
596 ? Turbine.getRealPath(path.substring(11))
597 // Add the path after the jar path separator again to the new url.
598 : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
599
600 log.debug("Result (absolute jar path): " + path);
601 }
602 }
603 else if(path.startsWith(ABSOLUTE_PREFIX))
604 {
605 // skip file:// -> 7 chars
606 path = Turbine.getRealPath(path.substring(7));
607
608 log.debug("Result (absolute URL Path): " + path);
609 }
610 // Test if this might be some sort of URL that we haven't encountered yet.
611 else if(path.indexOf("://") < 0)
612 {
613 path = Turbine.getRealPath(path);
614
615 log.debug("Result (normal fs reference): " + path);
616 }
617
618 log.debug("Adding " + key + " -> " + path);
619 // Re-Add this property to the configuration object
620 veloConfig.addProperty(key, path);
621 }
622 }
623 return veloConfig;
624 }
625
626 /**
627 * Find out if a given template exists. Velocity
628 * will do its own searching to determine whether
629 * a template exists or not.
630 *
631 * @param template String template to search for
632 * @return True if the template can be loaded by Velocity
633 */
634 @Override
635 public boolean templateExists(String template)
636 {
637 return Velocity.resourceExists(template);
638 }
639
640 /**
641 * Performs post-request actions (releases context
642 * tools back to the object pool).
643 *
644 * @param context a Velocity Context
645 */
646 public void requestFinished(Context context)
647 {
648 if (pullModelActive)
649 {
650 pullService.releaseTools(context);
651 }
652 }
653 }