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 }