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