View Javadoc

1   package org.apache.turbine.services.velocity;
2   
3   /*
4    * Copyright 2001-2005 The Apache Software Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License")
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.io.OutputStreamWriter;
23  import java.io.Writer;
24  
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Vector;
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  
37  import org.apache.velocity.VelocityContext;
38  import org.apache.velocity.app.Velocity;
39  import org.apache.velocity.app.event.EventCartridge;
40  import org.apache.velocity.app.event.MethodExceptionEventHandler;
41  import org.apache.velocity.context.Context;
42  import org.apache.velocity.runtime.log.SimpleLog4JLogSystem;
43  
44  import org.apache.turbine.Turbine;
45  import org.apache.turbine.services.InitializationException;
46  import org.apache.turbine.services.pull.PullService;
47  import org.apache.turbine.services.pull.TurbinePull;
48  import org.apache.turbine.services.template.BaseTemplateEngineService;
49  import org.apache.turbine.util.RunData;
50  import org.apache.turbine.util.TurbineException;
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 264152 2005-08-29 14:50:22Z henning $
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     /*** Internal Reference to the pull Service */
105     private PullService pullService = null;
106 
107 
108     /***
109      * Load all configured components and initialize them. This is
110      * a zero parameter variant which queries the Turbine Servlet
111      * for its config.
112      *
113      * @throws InitializationException Something went wrong in the init
114      *         stage
115      */
116     public void init()
117             throws InitializationException
118     {
119         try
120         {
121             initVelocity();
122 
123             // We can only load the Pull Model ToolBox
124             // if the Pull service has been listed in the TR.props
125             // and the service has successfully been initialized.
126             if (TurbinePull.isRegistered())
127             {
128                 pullModelActive = true;
129 
130                 pullService = TurbinePull.getService();
131 
132                 log.debug("Activated Pull Tools");
133             }
134 
135             // Register with the template service.
136             registerConfiguration(VelocityService.VELOCITY_EXTENSION);
137 
138             setInit(true);
139         }
140         catch (Exception e)
141         {
142             throw new InitializationException(
143                 "Failed to initialize TurbineVelocityService", e);
144         }
145     }
146 
147 
148     /***
149      * Inits the service using servlet parameters to obtain path to the
150      * configuration file.
151      *
152      * @param config The ServletConfiguration from Turbine
153      *
154      * @throws InitializationException Something went wrong when starting up.
155      * @deprecated use init() instead.
156      */
157     public void init(ServletConfig config)
158             throws InitializationException
159     {
160         init();
161     }
162 
163 
164     /***
165      * Create a Context object that also contains the globalContext.
166      *
167      * @return A Context object.
168      */
169     public Context getContext()
170     {
171         Context globalContext =
172                 pullModelActive ? pullService.getGlobalContext() : null;
173 
174         Context ctx = new VelocityContext(globalContext);
175         return ctx;
176     }
177 
178     /***
179      * This method returns a new, empty Context object.
180      *
181      * @return A Context Object.
182      */
183     public Context getNewContext()
184     {
185         Context ctx = new VelocityContext();
186 
187         // Attach an Event Cartridge to it, so we get exceptions
188         // while invoking methods from the Velocity Screens
189         EventCartridge ec = new EventCartridge();
190         ec.addEventHandler(this);
191         ec.attachToContext(ctx);
192         return ctx;
193     }
194 
195     /***
196      * MethodException Event Cartridge handler
197      * for Velocity.
198      *
199      * It logs an execption thrown by the velocity processing
200      * on error level into the log file
201      *
202      * @param clazz The class that threw the exception
203      * @param method The Method name that threw the exception
204      * @param e The exception that would've been thrown
205      * @return A valid value to be used as Return value
206      * @throws Exception We threw the exception further up
207      */
208     public Object methodException(Class clazz, String method, Exception e)
209             throws Exception
210     {
211         log.error("Class " + clazz.getName() + "." + method + " threw Exception", e);
212 
213         if (!catchErrors)
214         {
215             throw e;
216         }
217 
218         return "[Turbine caught an Error here. Look into the turbine.log for further information]";
219     }
220 
221     /***
222      * Create a Context from the RunData object.  Adds a pointer to
223      * the RunData object to the VelocityContext so that RunData
224      * is available in the templates.
225      *
226      * @param data The Turbine RunData object.
227      * @return A clone of the WebContext needed by Velocity.
228      */
229     public Context getContext(RunData data)
230     {
231         // Attempt to get it from the data first.  If it doesn't
232         // exist, create it and then stuff it into the data.
233         Context context = (Context)
234             data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
235 
236         if (context == null)
237         {
238             context = getContext();
239             context.put(VelocityService.RUNDATA_KEY, data);
240 
241             if (pullModelActive)
242             {
243                 // Populate the toolbox with request scope, session scope
244                 // and persistent scope tools (global tools are already in
245                 // the toolBoxContent which has been wrapped to construct
246                 // this request-specific context).
247                 pullService.populateContext(context, data);
248             }
249 
250             data.getTemplateInfo().setTemplateContext(
251                 VelocityService.CONTEXT, context);
252         }
253         return context;
254     }
255 
256     /***
257      * Process the request and fill in the template with the values
258      * you set in the Context.
259      *
260      * @param context  The populated context.
261      * @param filename The file name of the template.
262      * @return The process template as a String.
263      *
264      * @throws TurbineException Any exception trown while processing will be
265      *         wrapped into a TurbineException and rethrown.
266      */
267     public String handleRequest(Context context, String filename)
268         throws TurbineException
269     {
270         String results = null;
271         ByteArrayOutputStream bytes = null;
272         OutputStreamWriter writer = null;
273         String charset = getCharSet(context);
274 
275         try
276         {
277             bytes = new ByteArrayOutputStream();
278 
279             writer = new OutputStreamWriter(bytes, charset);
280 
281             executeRequest(context, filename, writer);
282             writer.flush();
283             results = bytes.toString(charset);
284         }
285         catch (Exception e)
286         {
287             renderingError(filename, e);
288         }
289         finally
290         {
291             try
292             {
293                 if (bytes != null)
294                 {
295                     bytes.close();
296                 }
297             }
298             catch (IOException ignored)
299             {
300                 // do nothing.
301             }
302         }
303         return results;
304     }
305 
306     /***
307      * Process the request and fill in the template with the values
308      * you set in the Context.
309      *
310      * @param context A Context.
311      * @param filename A String with the filename of the template.
312      * @param output A OutputStream where we will write the process template as
313      * a String.
314      *
315      * @throws TurbineException Any exception trown while processing will be
316      *         wrapped into a TurbineException and rethrown.
317      */
318     public void handleRequest(Context context, String filename,
319                               OutputStream output)
320             throws TurbineException
321     {
322         String charset  = getCharSet(context);
323         OutputStreamWriter writer = null;
324 
325         try
326         {
327             writer = new OutputStreamWriter(output, charset);
328             executeRequest(context, filename, writer);
329         }
330         catch (Exception e)
331         {
332             renderingError(filename, e);
333         }
334         finally
335         {
336             try
337             {
338                 if (writer != null)
339                 {
340                     writer.flush();
341                 }
342             }
343             catch (Exception ignored)
344             {
345                 // do nothing.
346             }
347         }
348     }
349 
350 
351     /***
352      * Process the request and fill in the template with the values
353      * you set in the Context.
354      *
355      * @param context A Context.
356      * @param filename A String with the filename of the template.
357      * @param writer A Writer where we will write the process template as
358      * a String.
359      *
360      * @throws TurbineException Any exception trown while processing will be
361      *         wrapped into a TurbineException and rethrown.
362      */
363     public void handleRequest(Context context, String filename, Writer writer)
364             throws TurbineException
365     {
366         try
367         {
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. Apply the character and template
394      * encodings from RunData to the result.
395      *
396      * @param context A Context.
397      * @param filename A String with the filename of the template.
398      * @param writer A OutputStream where we will write the process template as
399      * a String.
400      *
401      * @throws Exception A problem occured.
402      */
403     private void executeRequest(Context context, String filename,
404                                 Writer writer)
405             throws Exception
406     {
407         String encoding = getEncoding(context);
408 
409         if (encoding != null)
410         {
411             Velocity.mergeTemplate(filename, encoding, context, writer);
412         }
413         else
414         {
415             Velocity.mergeTemplate(filename, context, writer);
416         }
417     }
418 
419     /***
420      * Retrieve the required charset from the Turbine RunData in the context
421      *
422      * @param context A Context.
423      * @return The character set applied to the resulting String.
424      */
425     private String getCharSet(Context context)
426     {
427         String charset = null;
428 
429         Object data = context.get(VelocityService.RUNDATA_KEY);
430         if ((data != null) && (data instanceof RunData))
431         {
432             charset = ((RunData) data).getCharSet();
433         }
434 
435         return (StringUtils.isEmpty(charset)) ? DEFAULT_CHAR_SET : charset;
436     }
437 
438     /***
439      * Retrieve the required encoding from the Turbine RunData in the context
440      *
441      * @param context A Context.
442      * @return The encoding applied to the resulting String.
443      */
444     private String getEncoding(Context context)
445     {
446         String encoding = null;
447 
448         Object data = context.get(VelocityService.RUNDATA_KEY);
449         if ((data != null) && (data instanceof RunData))
450         {
451             encoding = ((RunData) data).getTemplateEncoding();
452         }
453 
454         return encoding;
455     }
456 
457     /***
458      * Macro to handle rendering errors.
459      *
460      * @param filename The file name of the unrenderable template.
461      * @param e        The error.
462      *
463      * @exception TurbineException Thrown every time.  Adds additional
464      *                             information to <code>e</code>.
465      */
466     private static void renderingError(String filename, Exception e)
467             throws TurbineException
468     {
469         String err = "Error rendering Velocity template: " + filename;
470         log.error(err, e);
471         throw new TurbineException(err, e);
472     }
473 
474     /***
475      * Setup the velocity runtime by using a subset of the
476      * Turbine configuration which relates to velocity.
477      *
478      * @exception Exception An Error occured.
479      */
480     private synchronized void initVelocity()
481         throws Exception
482     {
483         // Get the configuration for this service.
484         Configuration conf = getConfiguration();
485 
486         catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
487 
488         conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
489                 SimpleLog4JLogSystem.class.getName());
490         conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM
491                 + ".log4j.category", "velocity");
492 
493         Velocity.setExtendedProperties(createVelocityProperties(conf));
494         Velocity.init();
495     }
496 
497 
498     /***
499      * This method generates the Extended Properties object necessary
500      * for the initialization of Velocity. It also converts the various
501      * resource loader pathes into webapp relative pathes. It also
502      *
503      * @param conf The Velocity Service configuration
504      *
505      * @return An ExtendedProperties Object for Velocity
506      *
507      * @throws Exception If a problem occured while converting the properties.
508      */
509 
510     public ExtendedProperties createVelocityProperties(Configuration conf)
511             throws Exception
512     {
513         // This bugger is public, because we want to run some Unit tests
514         // on it.
515 
516         ExtendedProperties veloConfig = new ExtendedProperties();
517 
518         // Fix up all the template resource loader pathes to be
519         // webapp relative. Copy all other keys verbatim into the
520         // veloConfiguration.
521 
522         for (Iterator i = conf.getKeys(); i.hasNext();)
523         {
524             String key = (String) i.next();
525             if (!key.endsWith(RESOURCE_LOADER_PATH))
526             {
527                 Object value = conf.getProperty(key);
528 
529                 // Since 1.0-pre-something, Commons Collections suddently
530                 // no longer returns a vector for multiple value-keys but a
531                 // List object. Velocity will choke if we add this object because
532                 // org.apache.commons.collections.ExtendedProperties expect a
533                 // Vector object. Ah, the joys of incompatible class changes,
534                 // unwritten assumptions and general Java JAR Hell... =8-O
535                 if (value instanceof List)
536                 {
537                     List srcValue = (List) value;
538                     Vector targetValue = new Vector(srcValue.size());
539 
540                     for (Iterator it = srcValue.iterator(); it.hasNext(); )
541                     {
542                         targetValue.add(it.next());
543                     }
544 
545                     veloConfig.addProperty(key, targetValue);
546                 }
547                 else
548                 {
549                     veloConfig.addProperty(key, value);
550                 }
551 
552                 continue; // for()
553             }
554 
555             List paths = conf.getList(key, null);
556             if (paths == null)
557             {
558                 // We don't copy this into VeloProperties, because
559                 // null value is unhealthy for the ExtendedProperties object...
560                 continue; // for()
561             }
562 
563             Velocity.clearProperty(key);
564 
565             // Translate the supplied pathes given here.
566             // the following three different kinds of
567             // pathes must be translated to be webapp-relative
568             //
569             // jar:file://path-component!/entry-component
570             // file://path-component
571             // path/component
572 
573             for (Iterator j = paths.iterator(); j.hasNext();)
574             {
575                 String path = (String) j.next();
576 
577                 log.debug("Translating " + path);
578 
579                 if (path.startsWith(JAR_PREFIX))
580                 {
581                     // skip jar: -> 4 chars
582                     if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
583                     {
584                         // We must convert up to the jar path separator
585                         int jarSepIndex = path.indexOf("!/");
586 
587                         // jar:file:// -> skip 11 chars
588                         path = (jarSepIndex < 0)
589                             ? Turbine.getRealPath(path.substring(11))
590                         // Add the path after the jar path separator again to the new url.
591                             : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
592 
593                         log.debug("Result (absolute jar path): " + path);
594                     }
595                 }
596                 else if (path.startsWith(ABSOLUTE_PREFIX))
597                 {
598                     // skip file:// -> 7 chars
599                     path = Turbine.getRealPath(path.substring(7));
600 
601                     log.debug("Result (absolute URL Path): " + path);
602                 }
603                 // Test if this might be some sort of URL that we haven't encountered yet.
604                 else if (path.indexOf("://") < 0)
605                 {
606                     path = Turbine.getRealPath(path);
607 
608                     log.debug("Result (normal fs reference): " + path);
609                 }
610 
611                 log.debug("Adding " + key + " -> " + path);
612                 // Re-Add this property to the configuration object
613                 veloConfig.addProperty(key, path);
614             }
615         }
616         return veloConfig;
617     }
618 
619     /***
620      * Find out if a given template exists. Velocity
621      * will do its own searching to determine whether
622      * a template exists or not.
623      *
624      * @param template String template to search for
625      * @return True if the template can be loaded by Velocity
626      */
627     public boolean templateExists(String template)
628     {
629         return Velocity.templateExists(template);
630     }
631 
632     /***
633      * Performs post-request actions (releases context
634      * tools back to the object pool).
635      *
636      * @param context a Velocity Context
637      */
638     public void requestFinished(Context context)
639     {
640         if (pullModelActive)
641         {
642             pullService.releaseTools(context);
643         }
644     }
645 }