View Javadoc

1   package org.apache.turbine.services.velocity;
2   
3   
4   /*
5    * Licensed to the Apache Software Foundation (ASF) under one
6    * or more contributor license agreements.  See the NOTICE file
7    * distributed with this work for additional information
8    * regarding copyright ownership.  The ASF licenses this file
9    * to you under the Apache License, Version 2.0 (the
10   * "License"); you may not use this file except in compliance
11   * with the License.  You may obtain a copy of the License at
12   *
13   *   http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing,
16   * software distributed under the License is distributed on an
17   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18   * KIND, either express or implied.  See the License for the
19   * specific language governing permissions and limitations
20   * under the License.
21   */
22  
23  
24  import java.io.ByteArrayOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStream;
27  import java.io.OutputStreamWriter;
28  import java.io.Writer;
29  import java.util.Iterator;
30  import java.util.List;
31  
32  import org.apache.commons.collections.ExtendedProperties;
33  import org.apache.commons.configuration.Configuration;
34  import org.apache.commons.lang.StringUtils;
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.apache.turbine.Turbine;
38  import org.apache.turbine.pipeline.PipelineData;
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   * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
77   * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
78   * @version $Id: TurbineVelocityService.java 1073172 2011-02-21 22:16:51Z tv $
79   */
80  public class TurbineVelocityService
81          extends BaseTemplateEngineService
82          implements VelocityService,
83                     MethodExceptionEventHandler
84  {
85      /** The generic resource loader path property in velocity.*/
86      private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
87  
88      /** Default character set to use if not specified in the RunData object. */
89      private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
90  
91      /** The prefix used for URIs which are of type <code>jar</code>. */
92      private static final String JAR_PREFIX = "jar:";
93  
94      /** The prefix used for URIs which are of type <code>absolute</code>. */
95      private static final String ABSOLUTE_PREFIX = "file://";
96  
97      /** Logging */
98      private static Log log = LogFactory.getLog(TurbineVelocityService.class);
99  
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 }