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  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Vector;
30  
31  import javax.servlet.ServletConfig;
32  
33  import org.apache.commons.collections.ExtendedProperties;
34  import org.apache.commons.configuration.Configuration;
35  import org.apache.commons.lang.StringUtils;
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  import org.apache.turbine.Turbine;
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   * @version $Id: TurbineVelocityService.java 552336 2007-07-01 16:33:47Z tv $
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     /*** Default encoding to use if not specified in the RunData object. */
105     private String defaultEncoding = DEFAULT_CHAR_SET;
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         Velocity.mergeTemplate(filename, encoding, context, writer);
413     }
414 
415     /***
416      * Retrieve the required charset from the Turbine RunData in the context
417      *
418      * @param context A Context.
419      * @return The character set applied to the resulting String.
420      */
421     private String getCharSet(Context context)
422     {
423         String charset = null;
424 
425         Object data = context.get(VelocityService.RUNDATA_KEY);
426         if ((data != null) && (data instanceof RunData))
427         {
428             charset = ((RunData) data).getCharSet();
429         }
430 
431         return (StringUtils.isEmpty(charset)) ? DEFAULT_CHAR_SET : charset;
432     }
433 
434     /***
435      * Retrieve the required encoding from the Turbine RunData in the context
436      *
437      * @param context A Context.
438      * @return The encoding applied to the resulting String.
439      */
440     private String getEncoding(Context context)
441     {
442         String encoding = null;
443 
444         Object data = context.get(VelocityService.RUNDATA_KEY);
445         if ((data != null) && (data instanceof RunData))
446         {
447             encoding = ((RunData) data).getTemplateEncoding();
448         }
449 
450         return (StringUtils.isEmpty(encoding)) ? defaultEncoding : encoding;
451     }
452 
453     /***
454      * Macro to handle rendering errors.
455      *
456      * @param filename The file name of the unrenderable template.
457      * @param e        The error.
458      *
459      * @exception TurbineException Thrown every time.  Adds additional
460      *                             information to <code>e</code>.
461      */
462     private static void renderingError(String filename, Exception e)
463             throws TurbineException
464     {
465         String err = "Error rendering Velocity template: " + filename;
466         log.error(err, e);
467         throw new TurbineException(err, e);
468     }
469 
470     /***
471      * Setup the velocity runtime by using a subset of the
472      * Turbine configuration which relates to velocity.
473      *
474      * @exception Exception An Error occured.
475      */
476     private synchronized void initVelocity()
477         throws Exception
478     {
479         // Get the configuration for this service.
480         Configuration conf = getConfiguration();
481 
482         catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
483         
484         if (conf.containsKey(Velocity.INPUT_ENCODING))
485         {
486             defaultEncoding = conf.getString(Velocity.INPUT_ENCODING);
487         }
488 
489         conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
490                 Log4JLogChute.class.getName());
491         conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM
492                 + ".log4j.category", "velocity");
493 
494         Velocity.setExtendedProperties(createVelocityProperties(conf));
495         Velocity.init();
496     }
497 
498 
499     /***
500      * This method generates the Extended Properties object necessary
501      * for the initialization of Velocity. It also converts the various
502      * resource loader pathes into webapp relative pathes. It also
503      *
504      * @param conf The Velocity Service configuration
505      *
506      * @return An ExtendedProperties Object for Velocity
507      *
508      * @throws Exception If a problem occured while converting the properties.
509      */
510 
511     public ExtendedProperties createVelocityProperties(Configuration conf)
512             throws Exception
513     {
514         // This bugger is public, because we want to run some Unit tests
515         // on it.
516 
517         ExtendedProperties veloConfig = new ExtendedProperties();
518 
519         // Fix up all the template resource loader pathes to be
520         // webapp relative. Copy all other keys verbatim into the
521         // veloConfiguration.
522 
523         for (Iterator i = conf.getKeys(); i.hasNext();)
524         {
525             String key = (String) i.next();
526             if (!key.endsWith(RESOURCE_LOADER_PATH))
527             {
528                 Object value = conf.getProperty(key);
529 
530                 // Since 1.0-pre-something, Commons Collections suddently
531                 // no longer returns a vector for multiple value-keys but a
532                 // List object. Velocity will choke if we add this object because
533                 // org.apache.commons.collections.ExtendedProperties expect a
534                 // Vector object. Ah, the joys of incompatible class changes,
535                 // unwritten assumptions and general Java JAR Hell... =8-O
536                 if (value instanceof List)
537                 {
538                     List srcValue = (List) value;
539                     Vector targetValue = new Vector(srcValue.size());
540 
541                     for (Iterator it = srcValue.iterator(); it.hasNext(); )
542                     {
543                         targetValue.add(it.next());
544                     }
545 
546                     veloConfig.addProperty(key, targetValue);
547                 }
548                 else
549                 {
550                     veloConfig.addProperty(key, value);
551                 }
552 
553                 continue; // for()
554             }
555 
556             List paths = conf.getList(key, null);
557             if (paths == null)
558             {
559                 // We don't copy this into VeloProperties, because
560                 // null value is unhealthy for the ExtendedProperties object...
561                 continue; // for()
562             }
563 
564             Velocity.clearProperty(key);
565 
566             // Translate the supplied pathes given here.
567             // the following three different kinds of
568             // pathes must be translated to be webapp-relative
569             //
570             // jar:file://path-component!/entry-component
571             // file://path-component
572             // path/component
573 
574             for (Iterator j = paths.iterator(); j.hasNext();)
575             {
576                 String path = (String) j.next();
577 
578                 log.debug("Translating " + path);
579 
580                 if (path.startsWith(JAR_PREFIX))
581                 {
582                     // skip jar: -> 4 chars
583                     if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
584                     {
585                         // We must convert up to the jar path separator
586                         int jarSepIndex = path.indexOf("!/");
587 
588                         // jar:file:// -> skip 11 chars
589                         path = (jarSepIndex < 0)
590                             ? Turbine.getRealPath(path.substring(11))
591                         // Add the path after the jar path separator again to the new url.
592                             : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
593 
594                         log.debug("Result (absolute jar path): " + path);
595                     }
596                 }
597                 else if (path.startsWith(ABSOLUTE_PREFIX))
598                 {
599                     // skip file:// -> 7 chars
600                     path = Turbine.getRealPath(path.substring(7));
601 
602                     log.debug("Result (absolute URL Path): " + path);
603                 }
604                 // Test if this might be some sort of URL that we haven't encountered yet.
605                 else if (path.indexOf("://") < 0)
606                 {
607                     path = Turbine.getRealPath(path);
608 
609                     log.debug("Result (normal fs reference): " + path);
610                 }
611 
612                 log.debug("Adding " + key + " -> " + path);
613                 // Re-Add this property to the configuration object
614                 veloConfig.addProperty(key, path);
615             }
616         }
617         return veloConfig;
618     }
619 
620     /***
621      * Find out if a given template exists. Velocity
622      * will do its own searching to determine whether
623      * a template exists or not.
624      *
625      * @param template String template to search for
626      * @return True if the template can be loaded by Velocity
627      */
628     public boolean templateExists(String template)
629     {
630         return Velocity.resourceExists(template);
631     }
632 
633     /***
634      * Performs post-request actions (releases context
635      * tools back to the object pool).
636      *
637      * @param context a Velocity Context
638      */
639     public void requestFinished(Context context)
640     {
641         if (pullModelActive)
642         {
643             pullService.releaseTools(context);
644         }
645     }
646 }