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.TurbineServices;
41  import org.apache.turbine.services.pull.PullService;
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.VelocityEngine;
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.RuntimeConstants;
51  import org.apache.velocity.runtime.log.CommonsLogLogChute;
52  
53  /**
54   * This is a Service that can process Velocity templates from within a
55   * Turbine Screen. It is used in conjunction with the templating service
56   * as a Templating Engine for templates ending in "vm". It registers
57   * itself as translation engine with the template service and gets
58   * accessed from there. After configuring it in your properties, it
59   * should never be necessary to call methods from this service directly.
60   *
61   * Here's an example of how you might use it from a
62   * screen:<br>
63   *
64   * <code>
65   * Context context = TurbineVelocity.getContext(data);<br>
66   * context.put("message", "Hello from Turbine!");<br>
67   * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
68   * data.getPage().getBody().addElement(results);<br>
69   * </code>
70   *
71   * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
72   * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
73   * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
74   * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
75   * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
76   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
77   * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
78   * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
79   * @version $Id: TurbineVelocityService.java 1773378 2016-12-09 13:19: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 final Log log = LogFactory.getLog(TurbineVelocityService.class);
100 
101     /** Encoding used when reading the templates. */
102     private String defaultInputEncoding;
103 
104     /** Encoding used by the outputstream when handling the requests. */
105     private String defaultOutputEncoding;
106 
107     /** Is the pullModelActive? */
108     private boolean pullModelActive = false;
109 
110     /** Shall we catch Velocity Errors and report them in the log file? */
111     private boolean catchErrors = true;
112 
113     /** Velocity runtime instance */
114     private VelocityEngine velocity = null;
115 
116     /** Internal Reference to the pull Service */
117     private PullService pullService = null;
118 
119 
120     /**
121      * Load all configured components and initialize them. This is
122      * a zero parameter variant which queries the Turbine Servlet
123      * for its config.
124      *
125      * @throws InitializationException Something went wrong in the init
126      *         stage
127      */
128     @Override
129     public void init()
130             throws InitializationException
131     {
132         try
133         {
134             initVelocity();
135 
136             // We can only load the Pull Model ToolBox
137             // if the Pull service has been listed in the TR.props
138             // and the service has successfully been initialized.
139             if (TurbineServices.getInstance().isRegistered(PullService.SERVICE_NAME))
140             {
141                 pullModelActive = true;
142                 pullService = (PullService)TurbineServices.getInstance().getService(PullService.SERVICE_NAME);
143 
144                 log.debug("Activated Pull Tools");
145             }
146 
147             // Register with the template service.
148             registerConfiguration(VelocityService.VELOCITY_EXTENSION);
149 
150             defaultInputEncoding = getConfiguration().getString("input.encoding", DEFAULT_CHAR_SET);
151             defaultOutputEncoding = getConfiguration().getString("output.encoding", defaultInputEncoding);
152 
153             setInit(true);
154         }
155         catch (Exception e)
156         {
157             throw new InitializationException(
158                 "Failed to initialize TurbineVelocityService", e);
159         }
160     }
161 
162     /**
163      * Create a Context object that also contains the globalContext.
164      *
165      * @return A Context object.
166      */
167     @Override
168     public Context getContext()
169     {
170         Context globalContext =
171                 pullModelActive ? pullService.getGlobalContext() : null;
172 
173         Context ctx = new VelocityContext(globalContext);
174         return ctx;
175     }
176 
177     /**
178      * This method returns a new, empty Context object.
179      *
180      * @return A Context Object.
181      */
182     @Override
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     @Override
209     @SuppressWarnings("rawtypes") // Interface not generified
210 	public Object methodException(Class clazz, String method, Exception e)
211             throws Exception
212     {
213         log.error("Class " + clazz.getName() + "." + method + " threw Exception", e);
214 
215         if (!catchErrors)
216         {
217             throw e;
218         }
219 
220         return "[Turbine caught an Error here. Look into the turbine.log for further information]";
221     }
222 
223     /**
224      * Create a Context from the PipelineData object.  Adds a pointer to
225      * the PipelineData object to the VelocityContext so that PipelineData
226      * is available in the templates.
227      *
228      * @param pipelineData The Turbine PipelineData object.
229      * @return A clone of the WebContext needed by Velocity.
230      */
231     @Override
232     public Context getContext(PipelineData pipelineData)
233     {
234         //Map runDataMap = (Map)pipelineData.get(RunData.class);
235         RunData data = (RunData)pipelineData;
236         // Attempt to get it from the data first.  If it doesn't
237         // exist, create it and then stuff it into the data.
238         Context context = (Context)
239             data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
240 
241         if (context == null)
242         {
243             context = getContext();
244             context.put(VelocityService.RUNDATA_KEY, data);
245             // we will add both data and pipelineData to the context.
246             context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
247 
248             if (pullModelActive)
249             {
250                 // Populate the toolbox with request scope, session scope
251                 // and persistent scope tools (global tools are already in
252                 // the toolBoxContent which has been wrapped to construct
253                 // this request-specific context).
254                 pullService.populateContext(context, pipelineData);
255             }
256 
257             data.getTemplateInfo().setTemplateContext(
258                 VelocityService.CONTEXT, context);
259         }
260         return context;
261     }
262 
263     /**
264      * Process the request and fill in the template with the values
265      * you set in the Context.
266      *
267      * @param context  The populated context.
268      * @param filename The file name of the template.
269      * @return The process template as a String.
270      *
271      * @throws TurbineException Any exception thrown while processing will be
272      *         wrapped into a TurbineException and rethrown.
273      */
274     @Override
275     public String handleRequest(Context context, String filename)
276         throws TurbineException
277     {
278         String results = null;
279         ByteArrayOutputStream bytes = null;
280         OutputStreamWriter writer = null;
281         String charset = getOutputCharSet(context);
282 
283         try
284         {
285             bytes = new ByteArrayOutputStream();
286 
287             writer = new OutputStreamWriter(bytes, charset);
288 
289             executeRequest(context, filename, writer);
290             writer.flush();
291             results = bytes.toString(charset);
292         }
293         catch (Exception e)
294         {
295             renderingError(filename, e);
296         }
297         finally
298         {
299             try
300             {
301                 if (bytes != null)
302                 {
303                     bytes.close();
304                 }
305             }
306             catch (IOException ignored)
307             {
308                 // do nothing.
309             }
310         }
311         return results;
312     }
313 
314     /**
315      * Process the request and fill in the template with the values
316      * you set in the Context.
317      *
318      * @param context A Context.
319      * @param filename A String with the filename of the template.
320      * @param output A OutputStream where we will write the process template as
321      * a String.
322      *
323      * @throws TurbineException Any exception thrown while processing will be
324      *         wrapped into a TurbineException and rethrown.
325      */
326     @Override
327     public void handleRequest(Context context, String filename,
328                               OutputStream output)
329             throws TurbineException
330     {
331         String charset  = getOutputCharSet(context);
332         OutputStreamWriter writer = null;
333 
334         try
335         {
336             writer = new OutputStreamWriter(output, charset);
337             executeRequest(context, filename, writer);
338         }
339         catch (Exception e)
340         {
341             renderingError(filename, e);
342         }
343         finally
344         {
345             try
346             {
347                 if (writer != null)
348                 {
349                     writer.flush();
350                 }
351             }
352             catch (Exception ignored)
353             {
354                 // do nothing.
355             }
356         }
357     }
358 
359 
360     /**
361      * Process the request and fill in the template with the values
362      * you set in the Context.
363      *
364      * @param context A Context.
365      * @param filename A String with the filename of the template.
366      * @param writer A Writer where we will write the process template as
367      * a String.
368      *
369      * @throws TurbineException Any exception thrown while processing will be
370      *         wrapped into a TurbineException and rethrown.
371      */
372     @Override
373     public void handleRequest(Context context, String filename, Writer writer)
374             throws TurbineException
375     {
376         try
377         {
378             executeRequest(context, filename, writer);
379         }
380         catch (Exception e)
381         {
382             renderingError(filename, e);
383         }
384         finally
385         {
386             try
387             {
388                 if (writer != null)
389                 {
390                     writer.flush();
391                 }
392             }
393             catch (Exception ignored)
394             {
395                 // do nothing.
396             }
397         }
398     }
399 
400 
401     /**
402      * Process the request and fill in the template with the values
403      * you set in the Context. Apply the character and template
404      * encodings from RunData to the result.
405      *
406      * @param context A Context.
407      * @param filename A String with the filename of the template.
408      * @param writer A OutputStream where we will write the process template as
409      * a String.
410      *
411      * @throws Exception A problem occurred.
412      */
413     private void executeRequest(Context context, String filename,
414                                 Writer writer)
415             throws Exception
416     {
417         String encoding = getTemplateEncoding(context);
418 
419         if (encoding == null)
420         {
421           encoding = defaultOutputEncoding;
422         }
423 
424 		velocity.mergeTemplate(filename, encoding, context, writer);
425     }
426 
427     /**
428      * Retrieve the required charset from the Turbine RunData in the context
429      *
430      * @param context A Context.
431      * @return The character set applied to the resulting String.
432      */
433     private String getOutputCharSet(Context context)
434     {
435         String charset = null;
436 
437         Object data = context.get(VelocityService.RUNDATA_KEY);
438         if ((data != null) && (data instanceof RunData))
439         {
440             charset = ((RunData) data).getCharSet();
441         }
442 
443         return (StringUtils.isEmpty(charset)) ? defaultOutputEncoding : charset;
444     }
445 
446     /**
447      * Retrieve the required encoding from the Turbine RunData in the context
448      *
449      * @param context A Context.
450      * @return The encoding applied to the resulting String.
451      */
452     private String getTemplateEncoding(Context context)
453     {
454         String encoding = null;
455 
456         Object data = context.get(VelocityService.RUNDATA_KEY);
457         if ((data != null) && (data instanceof RunData))
458         {
459             encoding = ((RunData) data).getTemplateEncoding();
460         }
461 
462         return encoding != null ? encoding : defaultInputEncoding;
463     }
464 
465     /**
466      * Macro to handle rendering errors.
467      *
468      * @param filename The file name of the unrenderable template.
469      * @param e        The error.
470      *
471      * @throws TurbineException Thrown every time.  Adds additional
472      *                             information to <code>e</code>.
473      */
474     private static final void renderingError(String filename, Exception e)
475             throws TurbineException
476     {
477         String err = "Error rendering Velocity template: " + filename;
478         log.error(err, e);
479         throw new TurbineException(err, e);
480     }
481 
482     /**
483      * Setup the velocity runtime by using a subset of the
484      * Turbine configuration which relates to velocity.
485      *
486      * @throws Exception An Error occurred.
487      */
488     private synchronized void initVelocity()
489         throws Exception
490     {
491         // Get the configuration for this service.
492         Configuration conf = getConfiguration();
493 
494         catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
495 
496         conf.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
497                 CommonsLogLogChute.class.getName());
498         conf.setProperty(CommonsLogLogChute.LOGCHUTE_COMMONS_LOG_NAME,
499                 "velocity");
500 
501         velocity = new VelocityEngine();
502         velocity.setExtendedProperties(createVelocityProperties(conf));
503         velocity.init();
504     }
505 
506 
507     /**
508      * This method generates the Extended Properties object necessary
509      * for the initialization of Velocity. It also converts the various
510      * resource loader pathes into webapp relative pathes. It also
511      *
512      * @param conf The Velocity Service configuration
513      *
514      * @return An ExtendedProperties Object for Velocity
515      *
516      * @throws Exception If a problem occurred while converting the properties.
517      */
518 
519     public ExtendedProperties createVelocityProperties(Configuration conf)
520             throws Exception
521     {
522         // This bugger is public, because we want to run some Unit tests
523         // on it.
524 
525         ExtendedProperties veloConfig = new ExtendedProperties();
526 
527         // Fix up all the template resource loader pathes to be
528         // webapp relative. Copy all other keys verbatim into the
529         // veloConfiguration.
530 
531         for (Iterator<String> i = conf.getKeys(); i.hasNext();)
532         {
533             String key = i.next();
534             if (!key.endsWith(RESOURCE_LOADER_PATH))
535             {
536                 Object value = conf.getProperty(key);
537                 if (value instanceof List<?>) {
538                     for (Iterator<?> itr = ((List<?>)value).iterator(); itr.hasNext();)
539                     {
540                         veloConfig.addProperty(key, itr.next());
541                     }
542                 }
543                 else
544                 {
545                     veloConfig.addProperty(key, value);
546                 }
547                 continue; // for()
548             }
549 
550             List<Object> paths = conf.getList(key, null);
551             if (paths == null)
552             {
553                 // We don't copy this into VeloProperties, because
554                 // null value is unhealthy for the ExtendedProperties object...
555                 continue; // for()
556             }
557 
558             // Translate the supplied pathes given here.
559             // the following three different kinds of
560             // pathes must be translated to be webapp-relative
561             //
562             // jar:file://path-component!/entry-component
563             // file://path-component
564             // path/component
565             for (Object p : paths)
566             {
567             	String path = (String)p;
568                 log.debug("Translating " + path);
569 
570                 if (path.startsWith(JAR_PREFIX))
571                 {
572                     // skip jar: -> 4 chars
573                     if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
574                     {
575                         // We must convert up to the jar path separator
576                         int jarSepIndex = path.indexOf("!/");
577 
578                         // jar:file:// -> skip 11 chars
579                         path = (jarSepIndex < 0)
580                             ? Turbine.getRealPath(path.substring(11))
581                         // Add the path after the jar path separator again to the new url.
582                             : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
583 
584                         log.debug("Result (absolute jar path): " + path);
585                     }
586                 }
587                 else if(path.startsWith(ABSOLUTE_PREFIX))
588                 {
589                     // skip file:// -> 7 chars
590                     path = Turbine.getRealPath(path.substring(7));
591 
592                     log.debug("Result (absolute URL Path): " + path);
593                 }
594                 // Test if this might be some sort of URL that we haven't encountered yet.
595                 else if(path.indexOf("://") < 0)
596                 {
597                     path = Turbine.getRealPath(path);
598 
599                     log.debug("Result (normal fs reference): " + path);
600                 }
601 
602                 log.debug("Adding " + key + " -> " + path);
603                 // Re-Add this property to the configuration object
604                 veloConfig.addProperty(key, path);
605             }
606         }
607         return veloConfig;
608     }
609 
610     /**
611      * Find out if a given template exists. Velocity
612      * will do its own searching to determine whether
613      * a template exists or not.
614      *
615      * @param template String template to search for
616      * @return True if the template can be loaded by Velocity
617      */
618     @Override
619     public boolean templateExists(String template)
620     {
621         return velocity.resourceExists(template);
622     }
623 
624     /**
625      * Performs post-request actions (releases context
626      * tools back to the object pool).
627      *
628      * @param context a Velocity Context
629      */
630     @Override
631     public void requestFinished(Context context)
632     {
633         if (pullModelActive)
634         {
635             pullService.releaseTools(context);
636         }
637     }
638 }