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