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.OutputStream;
26  import java.io.OutputStreamWriter;
27  import java.io.Writer;
28  import java.util.Iterator;
29  import java.util.List;
30  
31  import org.apache.commons.configuration2.Configuration;
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.apache.turbine.Turbine;
36  import org.apache.turbine.pipeline.PipelineData;
37  import org.apache.turbine.services.InitializationException;
38  import org.apache.turbine.services.TurbineServices;
39  import org.apache.turbine.services.pull.PullService;
40  import org.apache.turbine.services.template.BaseTemplateEngineService;
41  import org.apache.turbine.util.LocaleUtils;
42  import org.apache.turbine.util.RunData;
43  import org.apache.turbine.util.TurbineException;
44  import org.apache.velocity.VelocityContext;
45  import org.apache.velocity.app.VelocityEngine;
46  import org.apache.velocity.app.event.EventCartridge;
47  import org.apache.velocity.app.event.MethodExceptionEventHandler;
48  import org.apache.velocity.context.Context;
49  import org.apache.velocity.runtime.RuntimeConstants;
50  import org.apache.velocity.util.introspection.Info;
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 1854787 2019-03-04 18:30:25Z 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      /** The prefix used for URIs which are of type <code>jar</code>. */
89      private static final String JAR_PREFIX = "jar:";
90  
91      /** The prefix used for URIs which are of type <code>absolute</code>. */
92      private static final String ABSOLUTE_PREFIX = "file://";
93  
94      /** Logging */
95      private static final Logger log = LogManager.getLogger(TurbineVelocityService.class);
96  
97      /** Encoding used when reading the templates. */
98      private String defaultInputEncoding;
99  
100     /** Encoding used by the outputstream when handling the requests. */
101     private String defaultOutputEncoding;
102 
103     /** Is the pullModelActive? */
104     private boolean pullModelActive = false;
105 
106     /** Shall we catch Velocity Errors and report them in the log file? */
107     private boolean catchErrors = true;
108 
109     /** Velocity runtime instance */
110     private VelocityEngine velocity = null;
111 
112     /** Internal Reference to the pull Service */
113     private PullService pullService = null;
114 
115 
116     /**
117      * Load all configured components and initialize them. This is
118      * a zero parameter variant which queries the Turbine Servlet
119      * for its config.
120      *
121      * @throws InitializationException Something went wrong in the init
122      *         stage
123      */
124     @Override
125     public void init()
126             throws InitializationException
127     {
128         try
129         {
130             velocity = getInitializedVelocityEngine();
131 
132             // We can only load the Pull Model ToolBox
133             // if the Pull service has been listed in the TR.props
134             // and the service has successfully been initialized.
135             if (TurbineServices.getInstance().isRegistered(PullService.SERVICE_NAME))
136             {
137                 pullModelActive = true;
138                 pullService = (PullService)TurbineServices.getInstance().getService(PullService.SERVICE_NAME);
139 
140                 log.debug("Activated Pull Tools");
141             }
142 
143             // Register with the template service.
144             registerConfiguration(VelocityService.VELOCITY_EXTENSION);
145 
146             defaultInputEncoding = getConfiguration().getString("input.encoding", LocaleUtils.getDefaultInputEncoding());
147 
148             String outputEncoding = LocaleUtils.getOverrideCharSet();
149             if (outputEncoding == null)
150             {
151                 outputEncoding = defaultInputEncoding;
152             }
153             defaultOutputEncoding = getConfiguration().getString("output.encoding", defaultInputEncoding);
154 
155             setInit(true);
156         }
157         catch (Exception e)
158         {
159             throw new InitializationException(
160                 "Failed to initialize TurbineVelocityService", e);
161         }
162     }
163 
164     /**
165      * Create a Context object that also contains the globalContext.
166      *
167      * @return A Context object.
168      */
169     @Override
170     public Context getContext()
171     {
172         Context globalContext =
173                 pullModelActive ? pullService.getGlobalContext() : null;
174 
175         Context ctx = new VelocityContext(globalContext);
176         return ctx;
177     }
178 
179     /**
180      * This method returns a new, empty Context object.
181      *
182      * @return A Context Object.
183      */
184     @Override
185     public Context getNewContext()
186     {
187         Context ctx = new VelocityContext();
188 
189         // Attach an Event Cartridge to it, so we get exceptions
190         // while invoking methods from the Velocity Screens
191         EventCartridge ec = new EventCartridge();
192         ec.addEventHandler(this);
193         ec.attachToContext(ctx);
194         return ctx;
195     }
196 
197     /**
198      * MethodException Event Cartridge handler
199      * for Velocity.
200      *
201      * It logs an exception thrown by the velocity processing
202      * on error level into the log file
203      *
204      * @param context The current context
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      * @param info Information about the template, line and column the exception occurred
209      * @return A valid value to be used as Return value
210      */
211     @Override
212 	public Object methodException(Context context, @SuppressWarnings("rawtypes") Class clazz, String method, Exception e, Info info)
213     {
214         log.error("Class {}.{} threw Exception", clazz.getName(), method, e);
215 
216         if (!catchErrors)
217         {
218             throw new RuntimeException(e);
219         }
220 
221         return "[Turbine caught an Error in template " + info.getTemplateName()
222             + ", l:" + info.getLine()
223             + ", c:" + info.getColumn()
224             + ". Look into the turbine.log for further information]";
225     }
226 
227     /**
228      * Create a Context from the PipelineData object.  Adds a pointer to
229      * the PipelineData object to the VelocityContext so that PipelineData
230      * is available in the templates.
231      *
232      * @param pipelineData The Turbine PipelineData object.
233      * @return A clone of the WebContext needed by Velocity.
234      */
235     @Override
236     public Context getContext(PipelineData pipelineData)
237     {
238         //Map runDataMap = (Map)pipelineData.get(RunData.class);
239         RunData"../../../../../org/apache/turbine/util/RunData.html#RunData">RunData data = (RunData)pipelineData;
240         // Attempt to get it from the data first.  If it doesn't
241         // exist, create it and then stuff it into the data.
242         Context context = (Context)
243             data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
244 
245         if (context == null)
246         {
247             context = getContext();
248             context.put(VelocityService.RUNDATA_KEY, data);
249             // we will add both data and pipelineData to the context.
250             context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
251 
252             if (pullModelActive)
253             {
254                 // Populate the toolbox with request scope, session scope
255                 // and persistent scope tools (global tools are already in
256                 // the toolBoxContent which has been wrapped to construct
257                 // this request-specific context).
258                 pullService.populateContext(context, pipelineData);
259             }
260 
261             data.getTemplateInfo().setTemplateContext(
262                 VelocityService.CONTEXT, context);
263         }
264         return context;
265     }
266 
267     /**
268      * Process the request and fill in the template with the values
269      * you set in the Context.
270      *
271      * @param context  The populated context.
272      * @param filename The file name of the template.
273      * @return The process template as a String.
274      *
275      * @throws TurbineException Any exception thrown while processing will be
276      *         wrapped into a TurbineException and rethrown.
277      */
278     @Override
279     public String handleRequest(Context context, String filename)
280         throws TurbineException
281     {
282         String results = null;
283         OutputStreamWriter writer = null;
284         String charset = getOutputCharSet(context);
285 
286         try (ByteArrayOutputStream 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 
299         return results;
300     }
301 
302     /**
303      * Process the request and fill in the template with the values
304      * you set in the Context.
305      *
306      * @param context A Context.
307      * @param filename A String with the filename of the template.
308      * @param output A OutputStream where we will write the process template as
309      * a String.
310      *
311      * @throws TurbineException Any exception thrown while processing will be
312      *         wrapped into a TurbineException and rethrown.
313      */
314     @Override
315     public void handleRequest(Context context, String filename,
316                               OutputStream output)
317             throws TurbineException
318     {
319         String charset  = getOutputCharSet(context);
320 
321         try (OutputStreamWriter writer = new OutputStreamWriter(output, charset))
322         {
323             executeRequest(context, filename, writer);
324         }
325         catch (Exception e)
326         {
327             renderingError(filename, e);
328         }
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 writer A Writer where we will write the process template as
338      * a String.
339      *
340      * @throws TurbineException Any exception thrown while processing will be
341      *         wrapped into a TurbineException and rethrown.
342      */
343     @Override
344     public void handleRequest(Context context, String filename, Writer writer)
345             throws TurbineException
346     {
347         try
348         {
349             executeRequest(context, filename, writer);
350         }
351         catch (Exception e)
352         {
353             renderingError(filename, e);
354         }
355         finally
356         {
357             try
358             {
359                 if (writer != null)
360                 {
361                     writer.flush();
362                 }
363             }
364             catch (Exception ignored)
365             {
366                 // do nothing.
367             }
368         }
369     }
370 
371 
372     /**
373      * Process the request and fill in the template with the values
374      * you set in the Context. Apply the character and template
375      * encodings from RunData to the result.
376      *
377      * @param context A Context.
378      * @param filename A String with the filename of the template.
379      * @param writer A OutputStream where we will write the process template as
380      * a String.
381      *
382      * @throws Exception A problem occurred.
383      */
384     private void executeRequest(Context context, String filename,
385                                 Writer writer)
386             throws Exception
387     {
388         String encoding = getTemplateEncoding(context);
389 
390         if (encoding == null)
391         {
392           encoding = defaultOutputEncoding;
393         }
394 
395 		velocity.mergeTemplate(filename, encoding, context, writer);
396     }
397 
398     /**
399      * Retrieve the required charset from the Turbine RunData in the context
400      *
401      * @param context A Context.
402      * @return The character set applied to the resulting String.
403      */
404     private String getOutputCharSet(Context context)
405     {
406         String charset = null;
407 
408         Object data = context.get(VelocityService.RUNDATA_KEY);
409         if ((data != null) && (data instanceof RunData))
410         {
411             charset = ((RunData) data).getCharSet();
412         }
413 
414         return (StringUtils.isEmpty(charset)) ? defaultOutputEncoding : charset;
415     }
416 
417     /**
418      * Retrieve the required encoding from the Turbine RunData in the context
419      *
420      * @param context A Context.
421      * @return The encoding applied to the resulting String.
422      */
423     private String getTemplateEncoding(Context context)
424     {
425         String encoding = null;
426 
427         Object data = context.get(VelocityService.RUNDATA_KEY);
428         if ((data != null) && (data instanceof RunData))
429         {
430             encoding = ((RunData) data).getTemplateEncoding();
431         }
432 
433         return encoding != null ? encoding : defaultInputEncoding;
434     }
435 
436     /**
437      * Macro to handle rendering errors.
438      *
439      * @param filename The file name of the unrenderable template.
440      * @param e        The error.
441      *
442      * @throws TurbineException Thrown every time.  Adds additional
443      *                             information to <code>e</code>.
444      */
445     private static final void renderingError(String filename, Exception e)
446             throws TurbineException
447     {
448         String err = "Error rendering Velocity template: " + filename;
449         log.error(err, e);
450         throw new TurbineException(err, e);
451     }
452 
453     /**
454      * Setup the velocity runtime by using a subset of the
455      * Turbine configuration which relates to velocity.
456      *
457      * @throws Exception An Error occurred.
458      * @return an initialized VelocityEngine instance
459      */
460     private VelocityEngine getInitializedVelocityEngine()
461         throws Exception
462     {
463         // Get the configuration for this service.
464         Configuration conf = getConfiguration();
465 
466         catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
467 
468         // backward compatibility, can be overridden in the configuration
469         conf.setProperty(RuntimeConstants.RUNTIME_LOG_NAME, "velocity");
470 
471         VelocityEngine velocity = new VelocityEngine();
472         setVelocityProperties(velocity, conf);
473         velocity.init();
474 
475         return velocity;
476     }
477 
478 
479     /**
480      * This method generates the Properties object necessary
481      * for the initialization of Velocity. It also converts the various
482      * resource loader pathes into webapp relative pathes. It also
483      *
484      * @param velocity The Velocity engine
485      * @param conf The Velocity Service configuration
486      *
487      * @throws Exception If a problem occurred while converting the properties.
488      */
489 
490     protected void setVelocityProperties(VelocityEngine velocity, Configuration conf)
491             throws Exception
492     {
493         // Fix up all the template resource loader pathes to be
494         // webapp relative. Copy all other keys verbatim into the
495         // veloConfiguration.
496 
497         for (Iterator<String> i = conf.getKeys(); i.hasNext();)
498         {
499             String key = i.next();
500             if (!key.endsWith(RESOURCE_LOADER_PATH))
501             {
502                 Object value = conf.getProperty(key);
503                 if (value instanceof List<?>)
504                 {
505                     for (Iterator<?> itr = ((List<?>)value).iterator(); itr.hasNext();)
506                     {
507                         velocity.addProperty(key, itr.next());
508                     }
509                 }
510                 else
511                 {
512                     velocity.addProperty(key, value);
513                 }
514                 continue; // for()
515             }
516 
517             List<Object> paths = conf.getList(key, null);
518             if (paths == null)
519             {
520                 // We don't copy this into VeloProperties, because
521                 // null value is unhealthy for the ExtendedProperties object...
522                 continue; // for()
523             }
524 
525             // Translate the supplied pathes given here.
526             // the following three different kinds of
527             // pathes must be translated to be webapp-relative
528             //
529             // jar:file://path-component!/entry-component
530             // file://path-component
531             // path/component
532             for (Object p : paths)
533             {
534             	String path = (String)p;
535                 log.debug("Translating {}", path);
536 
537                 if (path.startsWith(JAR_PREFIX))
538                 {
539                     // skip jar: -> 4 chars
540                     if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
541                     {
542                         // We must convert up to the jar path separator
543                         int jarSepIndex = path.indexOf("!/");
544 
545                         // jar:file:// -> skip 11 chars
546                         path = (jarSepIndex < 0)
547                             ? Turbine.getRealPath(path.substring(11))
548                         // Add the path after the jar path separator again to the new url.
549                             : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
550 
551                         log.debug("Result (absolute jar path): {}", path);
552                     }
553                 }
554                 else if(path.startsWith(ABSOLUTE_PREFIX))
555                 {
556                     // skip file:// -> 7 chars
557                     path = Turbine.getRealPath(path.substring(7));
558 
559                     log.debug("Result (absolute URL Path): {}", path);
560                 }
561                 // Test if this might be some sort of URL that we haven't encountered yet.
562                 else if(path.indexOf("://") < 0)
563                 {
564                     path = Turbine.getRealPath(path);
565 
566                     log.debug("Result (normal fs reference): {}", path);
567                 }
568 
569                 log.debug("Adding {} -> {}", key, path);
570                 // Re-Add this property to the configuration object
571                 velocity.addProperty(key, path);
572             }
573         }
574     }
575 
576     /**
577      * Find out if a given template exists. Velocity
578      * will do its own searching to determine whether
579      * a template exists or not.
580      *
581      * @param template String template to search for
582      * @return True if the template can be loaded by Velocity
583      */
584     @Override
585     public boolean templateExists(String template)
586     {
587         return velocity.resourceExists(template);
588     }
589 
590     /**
591      * Performs post-request actions (releases context
592      * tools back to the object pool).
593      *
594      * @param context a Velocity Context
595      */
596     @Override
597     public void requestFinished(Context context)
598     {
599         if (pullModelActive)
600         {
601             pullService.releaseTools(context);
602         }
603     }
604 }