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