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 }