View Javadoc

1   package org.apache.turbine.services.velocity;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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             // 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     /***
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         // Attach an Event Cartridge to it, so we get exceptions
191         // while invoking methods from the Velocity Screens
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         // Attempt to get it from the data first.  If it doesn't
235         // exist, create it and then stuff it into the data.
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                 // Populate the toolbox with request scope, session scope
247                 // and persistent scope tools (global tools are already in
248                 // the toolBoxContent which has been wrapped to construct
249                 // this request-specific context).
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                 // do nothing.
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                 // do nothing.
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                 // do nothing.
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         // Get the configuration for this service.
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         // This bugger is public, because we want to run some Unit tests
517         // on it.
518 
519         ExtendedProperties veloConfig = new ExtendedProperties();
520 
521         // Fix up all the template resource loader pathes to be
522         // webapp relative. Copy all other keys verbatim into the
523         // veloConfiguration.
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                 // Since 1.0-pre-something, Commons Collections suddently
533                 // no longer returns a vector for multiple value-keys but a
534                 // List object. Velocity will choke if we add this object because
535                 // org.apache.commons.collections.ExtendedProperties expect a
536                 // Vector object. Ah, the joys of incompatible class changes,
537                 // unwritten assumptions and general Java JAR Hell... =8-O
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; // for()
556             }
557 
558             List paths = conf.getList(key, null);
559             if (paths == null)
560             {
561                 // We don't copy this into VeloProperties, because
562                 // null value is unhealthy for the ExtendedProperties object...
563                 continue; // for()
564             }
565 
566             Velocity.clearProperty(key);
567 
568             // Translate the supplied pathes given here.
569             // the following three different kinds of
570             // pathes must be translated to be webapp-relative
571             //
572             // jar:file://path-component!/entry-component
573             // file://path-component
574             // path/component
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                     // skip jar: -> 4 chars
585                     if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
586                     {
587                         // We must convert up to the jar path separator
588                         int jarSepIndex = path.indexOf("!/");
589 
590                         // jar:file:// -> skip 11 chars
591                         path = (jarSepIndex < 0)
592                             ? Turbine.getRealPath(path.substring(11))
593                         // Add the path after the jar path separator again to the new url.
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                     // skip file:// -> 7 chars
602                     path = Turbine.getRealPath(path.substring(7));
603 
604                     log.debug("Result (absolute URL Path): " + path);
605                 }
606                 // Test if this might be some sort of URL that we haven't encountered yet.
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                 // Re-Add this property to the configuration object
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 }