Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
TurbineVelocityService |
|
| 3.125;3,125 |
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 | 75 | 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 | 75 | 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 | 75 | private boolean pullModelActive = false; |
105 | ||
106 | /** Shall we catch Velocity Errors and report them in the log file? */ | |
107 | 75 | private boolean catchErrors = true; |
108 | ||
109 | /** Velocity runtime instance */ | |
110 | 75 | private VelocityEngine velocity = null; |
111 | ||
112 | /** Internal Reference to the pull Service */ | |
113 | 75 | 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 | 87 | 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 | 87 | if (TurbineServices.getInstance().isRegistered(PullService.SERVICE_NAME)) |
136 | { | |
137 | 87 | pullModelActive = true; |
138 | 87 | pullService = (PullService)TurbineServices.getInstance().getService(PullService.SERVICE_NAME); |
139 | ||
140 | 87 | log.debug("Activated Pull Tools"); |
141 | } | |
142 | ||
143 | // Register with the template service. | |
144 | 87 | registerConfiguration(VelocityService.VELOCITY_EXTENSION); |
145 | ||
146 | 87 | defaultInputEncoding = getConfiguration().getString("input.encoding", LocaleUtils.getDefaultInputEncoding()); |
147 | ||
148 | 87 | String outputEncoding = LocaleUtils.getOverrideCharSet(); |
149 | 87 | if (outputEncoding == null) |
150 | { | |
151 | 87 | outputEncoding = defaultInputEncoding; |
152 | } | |
153 | 87 | defaultOutputEncoding = getConfiguration().getString("output.encoding", defaultInputEncoding); |
154 | ||
155 | 87 | setInit(true); |
156 | } | |
157 | 0 | catch (Exception e) |
158 | { | |
159 | 0 | throw new InitializationException( |
160 | "Failed to initialize TurbineVelocityService", e); | |
161 | 87 | } |
162 | 87 | } |
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 | 27 | Context globalContext = |
173 | 27 | pullModelActive ? pullService.getGlobalContext() : null; |
174 | ||
175 | 27 | Context ctx = new VelocityContext(globalContext); |
176 | 27 | 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 | 87 | 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 | 87 | EventCartridge ec = new EventCartridge(); |
192 | 87 | ec.addEventHandler(this); |
193 | 87 | ec.attachToContext(ctx); |
194 | 87 | 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 | 0 | log.error("Class {}.{} threw Exception", clazz.getName(), method, e); |
215 | ||
216 | 0 | if (!catchErrors) |
217 | { | |
218 | 0 | throw new RuntimeException(e); |
219 | } | |
220 | ||
221 | 0 | return "[Turbine caught an Error in template " + info.getTemplateName() |
222 | 0 | + ", l:" + info.getLine() |
223 | 0 | + ", 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 | 84 | 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 | 75 | Context context = (Context) |
243 | 75 | data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT); |
244 | ||
245 | 75 | if (context == null) |
246 | { | |
247 | 27 | context = getContext(); |
248 | 27 | context.put(VelocityService.RUNDATA_KEY, data); |
249 | // we will add both data and pipelineData to the context. | |
250 | 27 | context.put(VelocityService.PIPELINEDATA_KEY, pipelineData); |
251 | ||
252 | 27 | 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 | 27 | pullService.populateContext(context, pipelineData); |
259 | } | |
260 | ||
261 | 27 | data.getTemplateInfo().setTemplateContext( |
262 | VelocityService.CONTEXT, context); | |
263 | } | |
264 | 75 | 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 | 6 | String results = null; |
283 | 6 | OutputStreamWriter writer = null; |
284 | 6 | String charset = getOutputCharSet(context); |
285 | ||
286 | 6 | try (ByteArrayOutputStream bytes = new ByteArrayOutputStream()) |
287 | { | |
288 | 6 | writer = new OutputStreamWriter(bytes, charset); |
289 | ||
290 | 6 | executeRequest(context, filename, writer); |
291 | 6 | writer.flush(); |
292 | 6 | results = bytes.toString(charset); |
293 | 6 | } |
294 | 0 | catch (Exception e) |
295 | { | |
296 | 0 | renderingError(filename, e); |
297 | 6 | } |
298 | ||
299 | 6 | 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 | 6 | String charset = getOutputCharSet(context); |
320 | ||
321 | 6 | try (OutputStreamWriter writer = new OutputStreamWriter(output, charset)) |
322 | { | |
323 | 6 | executeRequest(context, filename, writer); |
324 | 6 | } |
325 | 0 | catch (Exception e) |
326 | { | |
327 | 0 | renderingError(filename, e); |
328 | 6 | } |
329 | 6 | } |
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 | 0 | executeRequest(context, filename, writer); |
350 | } | |
351 | 0 | catch (Exception e) |
352 | { | |
353 | 0 | renderingError(filename, e); |
354 | } | |
355 | finally | |
356 | { | |
357 | 0 | try |
358 | { | |
359 | 0 | if (writer != null) |
360 | { | |
361 | 0 | writer.flush(); |
362 | } | |
363 | } | |
364 | 0 | catch (Exception ignored) |
365 | { | |
366 | // do nothing. | |
367 | 0 | } |
368 | 0 | } |
369 | 0 | } |
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 | 12 | String encoding = getTemplateEncoding(context); |
389 | ||
390 | 12 | if (encoding == null) |
391 | { | |
392 | 0 | encoding = defaultOutputEncoding; |
393 | } | |
394 | ||
395 | 12 | velocity.mergeTemplate(filename, encoding, context, writer); |
396 | 12 | } |
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 | 12 | String charset = null; |
407 | ||
408 | 12 | Object data = context.get(VelocityService.RUNDATA_KEY); |
409 | 12 | if ((data != null) && (data instanceof RunData)) |
410 | { | |
411 | 12 | charset = ((RunData) data).getCharSet(); |
412 | } | |
413 | ||
414 | 12 | 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 | 12 | String encoding = null; |
426 | ||
427 | 12 | Object data = context.get(VelocityService.RUNDATA_KEY); |
428 | 12 | if ((data != null) && (data instanceof RunData)) |
429 | { | |
430 | 12 | encoding = ((RunData) data).getTemplateEncoding(); |
431 | } | |
432 | ||
433 | 12 | 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 | 0 | String err = "Error rendering Velocity template: " + filename; |
449 | 0 | log.error(err, e); |
450 | 0 | 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 | 87 | Configuration conf = getConfiguration(); |
465 | ||
466 | 87 | catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT); |
467 | ||
468 | // backward compatibility, can be overridden in the configuration | |
469 | 87 | conf.setProperty(RuntimeConstants.RUNTIME_LOG_NAME, "velocity"); |
470 | ||
471 | 87 | VelocityEngine velocity = new VelocityEngine(); |
472 | 87 | setVelocityProperties(velocity, conf); |
473 | 87 | velocity.init(); |
474 | ||
475 | 87 | 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 | 90 | for (Iterator<String> i = conf.getKeys(); i.hasNext();) |
498 | { | |
499 | 1770 | String key = i.next(); |
500 | 1770 | if (!key.endsWith(RESOURCE_LOADER_PATH)) |
501 | { | |
502 | 1440 | Object value = conf.getProperty(key); |
503 | 1440 | if (value instanceof List<?>) |
504 | { | |
505 | 0 | for (Iterator<?> itr = ((List<?>)value).iterator(); itr.hasNext();) |
506 | { | |
507 | 0 | velocity.addProperty(key, itr.next()); |
508 | } | |
509 | } | |
510 | else | |
511 | { | |
512 | 1440 | velocity.addProperty(key, value); |
513 | } | |
514 | 1440 | continue; // for() |
515 | } | |
516 | ||
517 | 330 | List<Object> paths = conf.getList(key, null); |
518 | 330 | if (paths == null) |
519 | { | |
520 | // We don't copy this into VeloProperties, because | |
521 | // null value is unhealthy for the ExtendedProperties object... | |
522 | 0 | 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 | 330 | for (Object p : paths) |
533 | { | |
534 | 330 | String path = (String)p; |
535 | 330 | log.debug("Translating {}", path); |
536 | ||
537 | 330 | if (path.startsWith(JAR_PREFIX)) |
538 | { | |
539 | // skip jar: -> 4 chars | |
540 | 120 | if (path.substring(4).startsWith(ABSOLUTE_PREFIX)) |
541 | { | |
542 | // We must convert up to the jar path separator | |
543 | 90 | int jarSepIndex = path.indexOf("!/"); |
544 | ||
545 | // jar:file:// -> skip 11 chars | |
546 | 90 | path = (jarSepIndex < 0) |
547 | 30 | ? Turbine.getRealPath(path.substring(11)) |
548 | // Add the path after the jar path separator again to the new url. | |
549 | 60 | : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex)); |
550 | ||
551 | 90 | log.debug("Result (absolute jar path): {}", path); |
552 | 90 | } |
553 | } | |
554 | 210 | else if(path.startsWith(ABSOLUTE_PREFIX)) |
555 | { | |
556 | // skip file:// -> 7 chars | |
557 | 30 | path = Turbine.getRealPath(path.substring(7)); |
558 | ||
559 | 30 | 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 | 180 | else if(path.indexOf("://") < 0) |
563 | { | |
564 | 150 | path = Turbine.getRealPath(path); |
565 | ||
566 | 150 | log.debug("Result (normal fs reference): {}", path); |
567 | } | |
568 | ||
569 | 330 | log.debug("Adding {} -> {}", key, path); |
570 | // Re-Add this property to the configuration object | |
571 | 330 | velocity.addProperty(key, path); |
572 | 330 | } |
573 | 330 | } |
574 | 90 | } |
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 | 111 | 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 | 6 | if (pullModelActive) |
600 | { | |
601 | 6 | pullService.releaseTools(context); |
602 | } | |
603 | 6 | } |
604 | } |