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.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 1695634 2015-08-13 00:35:47Z 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 (TurbinePull.isRegistered())
140             {
141                 pullModelActive = true;
142 
143                 pullService = TurbinePull.getService();
144 
145                 log.debug("Activated Pull Tools");
146             }
147 
148             // Register with the template service.
149             registerConfiguration(VelocityService.VELOCITY_EXTENSION);
150 
151             defaultInputEncoding = getConfiguration().getString("input.encoding", DEFAULT_CHAR_SET);
152             defaultOutputEncoding = getConfiguration().getString("output.encoding", defaultInputEncoding);
153 
154             setInit(true);
155         }
156         catch (Exception e)
157         {
158             throw new InitializationException(
159                 "Failed to initialize TurbineVelocityService", e);
160         }
161     }
162 
163     /**
164      * Create a Context object that also contains the globalContext.
165      *
166      * @return A Context object.
167      */
168     @Override
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     @Override
184     public Context getNewContext()
185     {
186         Context ctx = new VelocityContext();
187 
188         // Attach an Event Cartridge to it, so we get exceptions
189         // while invoking methods from the Velocity Screens
190         EventCartridge ec = new EventCartridge();
191         ec.addEventHandler(this);
192         ec.attachToContext(ctx);
193         return ctx;
194     }
195 
196     /**
197      * MethodException Event Cartridge handler
198      * for Velocity.
199      *
200      * It logs an execption thrown by the velocity processing
201      * on error level into the log file
202      *
203      * @param clazz The class that threw the exception
204      * @param method The Method name that threw the exception
205      * @param e The exception that would've been thrown
206      * @return A valid value to be used as Return value
207      * @throws Exception We threw the exception further up
208      */
209     @Override
210     @SuppressWarnings("rawtypes") // Interface not generified
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 PipelineData object.  Adds a pointer to
226      * the PipelineData object to the VelocityContext so that PipelineData
227      * is available in the templates.
228      *
229      * @param pipelineData The Turbine PipelineData object.
230      * @return A clone of the WebContext needed by Velocity.
231      */
232     @Override
233     public Context getContext(PipelineData pipelineData)
234     {
235         //Map runDataMap = (Map)pipelineData.get(RunData.class);
236         RunData data = (RunData)pipelineData;
237         // Attempt to get it from the data first.  If it doesn't
238         // exist, create it and then stuff it into the data.
239         Context context = (Context)
240             data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
241 
242         if (context == null)
243         {
244             context = getContext();
245             context.put(VelocityService.RUNDATA_KEY, data);
246             // we will add both data and pipelineData to the context.
247             context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
248 
249             if (pullModelActive)
250             {
251                 // Populate the toolbox with request scope, session scope
252                 // and persistent scope tools (global tools are already in
253                 // the toolBoxContent which has been wrapped to construct
254                 // this request-specific context).
255                 pullService.populateContext(context, pipelineData);
256             }
257 
258             data.getTemplateInfo().setTemplateContext(
259                 VelocityService.CONTEXT, context);
260         }
261         return context;
262     }
263 
264     /**
265      * Process the request and fill in the template with the values
266      * you set in the Context.
267      *
268      * @param context  The populated context.
269      * @param filename The file name of the template.
270      * @return The process template as a String.
271      *
272      * @throws TurbineException Any exception thrown while processing will be
273      *         wrapped into a TurbineException and rethrown.
274      */
275     @Override
276     public String handleRequest(Context context, String filename)
277         throws TurbineException
278     {
279         String results = null;
280         ByteArrayOutputStream bytes = null;
281         OutputStreamWriter writer = null;
282         String charset = getOutputCharSet(context);
283 
284         try
285         {
286             bytes = new ByteArrayOutputStream();
287 
288             writer = new OutputStreamWriter(bytes, charset);
289 
290             executeRequest(context, filename, writer);
291             writer.flush();
292             results = bytes.toString(charset);
293         }
294         catch (Exception e)
295         {
296             renderingError(filename, e);
297         }
298         finally
299         {
300             try
301             {
302                 if (bytes != null)
303                 {
304                     bytes.close();
305                 }
306             }
307             catch (IOException ignored)
308             {
309                 // do nothing.
310             }
311         }
312         return results;
313     }
314 
315     /**
316      * Process the request and fill in the template with the values
317      * you set in the Context.
318      *
319      * @param context A Context.
320      * @param filename A String with the filename of the template.
321      * @param output A OutputStream where we will write the process template as
322      * a String.
323      *
324      * @throws TurbineException Any exception thrown while processing will be
325      *         wrapped into a TurbineException and rethrown.
326      */
327     @Override
328     public void handleRequest(Context context, String filename,
329                               OutputStream output)
330             throws TurbineException
331     {
332         String charset  = getOutputCharSet(context);
333         OutputStreamWriter writer = null;
334 
335         try
336         {
337             writer = new OutputStreamWriter(output, charset);
338             executeRequest(context, filename, writer);
339         }
340         catch (Exception e)
341         {
342             renderingError(filename, e);
343         }
344         finally
345         {
346             try
347             {
348                 if (writer != null)
349                 {
350                     writer.flush();
351                 }
352             }
353             catch (Exception ignored)
354             {
355                 // do nothing.
356             }
357         }
358     }
359 
360 
361     /**
362      * Process the request and fill in the template with the values
363      * you set in the Context.
364      *
365      * @param context A Context.
366      * @param filename A String with the filename of the template.
367      * @param writer A Writer where we will write the process template as
368      * a String.
369      *
370      * @throws TurbineException Any exception thrown while processing will be
371      *         wrapped into a TurbineException and rethrown.
372      */
373     @Override
374     public void handleRequest(Context context, String filename, Writer writer)
375             throws TurbineException
376     {
377         try
378         {
379             executeRequest(context, filename, writer);
380         }
381         catch (Exception e)
382         {
383             renderingError(filename, e);
384         }
385         finally
386         {
387             try
388             {
389                 if (writer != null)
390                 {
391                     writer.flush();
392                 }
393             }
394             catch (Exception ignored)
395             {
396                 // do nothing.
397             }
398         }
399     }
400 
401 
402     /**
403      * Process the request and fill in the template with the values
404      * you set in the Context. Apply the character and template
405      * encodings from RunData to the result.
406      *
407      * @param context A Context.
408      * @param filename A String with the filename of the template.
409      * @param writer A OutputStream where we will write the process template as
410      * a String.
411      *
412      * @throws Exception A problem occurred.
413      */
414     private void executeRequest(Context context, String filename,
415                                 Writer writer)
416             throws Exception
417     {
418         String encoding = getTemplateEncoding(context);
419 
420         if (encoding == null)
421         {
422           encoding = defaultOutputEncoding;
423         }
424 
425 		velocity.mergeTemplate(filename, encoding, context, writer);
426     }
427 
428     /**
429      * Retrieve the required charset from the Turbine RunData in the context
430      *
431      * @param context A Context.
432      * @return The character set applied to the resulting String.
433      */
434     private String getOutputCharSet(Context context)
435     {
436         String charset = null;
437 
438         Object data = context.get(VelocityService.RUNDATA_KEY);
439         if ((data != null) && (data instanceof RunData))
440         {
441             charset = ((RunData) data).getCharSet();
442         }
443 
444         return (StringUtils.isEmpty(charset)) ? defaultOutputEncoding : charset;
445     }
446 
447     /**
448      * Retrieve the required encoding from the Turbine RunData in the context
449      *
450      * @param context A Context.
451      * @return The encoding applied to the resulting String.
452      */
453     private String getTemplateEncoding(Context context)
454     {
455         String encoding = null;
456 
457         Object data = context.get(VelocityService.RUNDATA_KEY);
458         if ((data != null) && (data instanceof RunData))
459         {
460             encoding = ((RunData) data).getTemplateEncoding();
461         }
462 
463         return encoding != null ? encoding : defaultInputEncoding;
464     }
465 
466     /**
467      * Macro to handle rendering errors.
468      *
469      * @param filename The file name of the unrenderable template.
470      * @param e        The error.
471      *
472      * @exception TurbineException Thrown every time.  Adds additional
473      *                             information to <code>e</code>.
474      */
475     private static final void renderingError(String filename, Exception e)
476             throws TurbineException
477     {
478         String err = "Error rendering Velocity template: " + filename;
479         log.error(err, e);
480         throw new TurbineException(err, e);
481     }
482 
483     /**
484      * Setup the velocity runtime by using a subset of the
485      * Turbine configuration which relates to velocity.
486      *
487      * @exception Exception An Error occurred.
488      */
489     private synchronized void initVelocity()
490         throws Exception
491     {
492         // Get the configuration for this service.
493         Configuration conf = getConfiguration();
494 
495         catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
496 
497         conf.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
498                 CommonsLogLogChute.class.getName());
499         conf.setProperty(CommonsLogLogChute.LOGCHUTE_COMMONS_LOG_NAME,
500                 "velocity");
501 
502         velocity = new VelocityEngine();
503         velocity.setExtendedProperties(createVelocityProperties(conf));
504         velocity.init();
505     }
506 
507 
508     /**
509      * This method generates the Extended Properties object necessary
510      * for the initialization of Velocity. It also converts the various
511      * resource loader pathes into webapp relative pathes. It also
512      *
513      * @param conf The Velocity Service configuration
514      *
515      * @return An ExtendedProperties Object for Velocity
516      *
517      * @throws Exception If a problem occurred while converting the properties.
518      */
519 
520     public ExtendedProperties createVelocityProperties(Configuration conf)
521             throws Exception
522     {
523         // This bugger is public, because we want to run some Unit tests
524         // on it.
525 
526         ExtendedProperties veloConfig = new ExtendedProperties();
527 
528         // Fix up all the template resource loader pathes to be
529         // webapp relative. Copy all other keys verbatim into the
530         // veloConfiguration.
531 
532         for (Iterator<String> i = conf.getKeys(); i.hasNext();)
533         {
534             String key = i.next();
535             if (!key.endsWith(RESOURCE_LOADER_PATH))
536             {
537                 Object value = conf.getProperty(key);
538                 if (value instanceof List<?>) {
539                     for (Iterator<?> itr = ((List<?>)value).iterator(); itr.hasNext();)
540                     {
541                         veloConfig.addProperty(key, itr.next());
542                     }
543                 }
544                 else
545                 {
546                     veloConfig.addProperty(key, value);
547                 }
548                 continue; // for()
549             }
550 
551             List<Object> paths = conf.getList(key, null);
552             if (paths == null)
553             {
554                 // We don't copy this into VeloProperties, because
555                 // null value is unhealthy for the ExtendedProperties object...
556                 continue; // for()
557             }
558 
559             // Translate the supplied pathes given here.
560             // the following three different kinds of
561             // pathes must be translated to be webapp-relative
562             //
563             // jar:file://path-component!/entry-component
564             // file://path-component
565             // path/component
566             for (Object p : paths)
567             {
568             	String path = (String)p;
569                 log.debug("Translating " + path);
570 
571                 if (path.startsWith(JAR_PREFIX))
572                 {
573                     // skip jar: -> 4 chars
574                     if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
575                     {
576                         // We must convert up to the jar path separator
577                         int jarSepIndex = path.indexOf("!/");
578 
579                         // jar:file:// -> skip 11 chars
580                         path = (jarSepIndex < 0)
581                             ? Turbine.getRealPath(path.substring(11))
582                         // Add the path after the jar path separator again to the new url.
583                             : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
584 
585                         log.debug("Result (absolute jar path): " + path);
586                     }
587                 }
588                 else if(path.startsWith(ABSOLUTE_PREFIX))
589                 {
590                     // skip file:// -> 7 chars
591                     path = Turbine.getRealPath(path.substring(7));
592 
593                     log.debug("Result (absolute URL Path): " + path);
594                 }
595                 // Test if this might be some sort of URL that we haven't encountered yet.
596                 else if(path.indexOf("://") < 0)
597                 {
598                     path = Turbine.getRealPath(path);
599 
600                     log.debug("Result (normal fs reference): " + path);
601                 }
602 
603                 log.debug("Adding " + key + " -> " + path);
604                 // Re-Add this property to the configuration object
605                 veloConfig.addProperty(key, path);
606             }
607         }
608         return veloConfig;
609     }
610 
611     /**
612      * Find out if a given template exists. Velocity
613      * will do its own searching to determine whether
614      * a template exists or not.
615      *
616      * @param template String template to search for
617      * @return True if the template can be loaded by Velocity
618      */
619     @Override
620     public boolean templateExists(String template)
621     {
622         return velocity.resourceExists(template);
623     }
624 
625     /**
626      * Performs post-request actions (releases context
627      * tools back to the object pool).
628      *
629      * @param context a Velocity Context
630      */
631     @Override
632     public void requestFinished(Context context)
633     {
634         if (pullModelActive)
635         {
636             pullService.releaseTools(context);
637         }
638     }
639 }