View Javadoc

1   package org.apache.turbine.services.velocity;
2   
3   
4   /*
5    * Copyright 2001-2004 The Apache Software Foundation.
6    *
7    * Licensed under the Apache License, Version 2.0 (the "License")
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
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             // We can only load the Pull Model ToolBox
125             // if the Pull service has been listed in the TR.props
126             // and the service has successfully been initialized.
127             if (TurbinePull.isRegistered())
128             {
129                 pullModelActive = true;
130 
131                 pullService = TurbinePull.getService();
132 
133                 log.debug("Activated Pull Tools");
134             }
135 
136             // Register with the template service.
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         // Attach an Event Cartridge to it, so we get exceptions
189         // while invoking methods from the Velocity Screens
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         // Attempt to get it from the data first.  If it doesn't
233         // exist, create it and then stuff it into the data.
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                 // Populate the toolbox with request scope, session scope
245                 // and persistent scope tools (global tools are already in
246                 // the toolBoxContent which has been wrapped to construct
247                 // this request-specific context).
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         //Map runDataMap = (Map)pipelineData.get(RunData.class);
268         RunData data = (RunData)pipelineData;
269         // Attempt to get it from the data first.  If it doesn't
270         // exist, create it and then stuff it into the data.
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             // we will add both data and pipelineData to the context.
279             context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
280 
281             if (pullModelActive)
282             {
283                 // Populate the toolbox with request scope, session scope
284                 // and persistent scope tools (global tools are already in
285                 // the toolBoxContent which has been wrapped to construct
286                 // this request-specific context).
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                 // do nothing.
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                 // do nothing.
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                 // do nothing.
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         // Get the configuration for this service.
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         // This bugger is public, because we want to run some Unit tests
551         // on it.
552 
553         ExtendedProperties veloConfig = new ExtendedProperties();
554 
555         // Fix up all the template resource loader pathes to be
556         // webapp relative. Copy all other keys verbatim into the
557         // veloConfiguration.
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; // for()
576             }
577 
578             List paths = conf.getList(key, null);
579             if (paths == null)
580             {
581                 // We don't copy this into VeloProperties, because
582                 // null value is unhealthy for the ExtendedProperties object...
583                 continue; // for()
584             }
585 
586             Velocity.clearProperty(key);
587 
588             // Translate the supplied pathes given here.
589             // the following three different kinds of
590             // pathes must be translated to be webapp-relative
591             //
592             // jar:file://path-component!/entry-component
593             // file://path-component
594             // path/component
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                     // skip jar: -> 4 chars
604                     if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
605                     {
606                         // We must convert up to the jar path separator
607                         int jarSepIndex = path.indexOf("!/");
608 
609                         // jar:file:// -> skip 11 chars
610                         path = (jarSepIndex < 0)
611                             ? Turbine.getRealPath(path.substring(11))
612                         // Add the path after the jar path separator again to the new url.
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                     // skip file:// -> 7 chars
621                     path = Turbine.getRealPath(path.substring(7));
622 
623                     log.debug("Result (absolute URL Path): " + path);
624                 }
625                 // Test if this might be some sort of URL that we haven't encountered yet.
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                 // Re-Add this property to the configuration object
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 }