001package org.apache.turbine.services.velocity; 002 003 004/* 005 * Licensed to the Apache Software Foundation (ASF) under one 006 * or more contributor license agreements. See the NOTICE file 007 * distributed with this work for additional information 008 * regarding copyright ownership. The ASF licenses this file 009 * to you under the Apache License, Version 2.0 (the 010 * "License"); you may not use this file except in compliance 011 * with the License. You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, 016 * software distributed under the License is distributed on an 017 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 018 * KIND, either express or implied. See the License for the 019 * specific language governing permissions and limitations 020 * under the License. 021 */ 022 023 024import java.io.ByteArrayOutputStream; 025import java.io.OutputStream; 026import java.io.OutputStreamWriter; 027import java.io.Writer; 028import java.util.Iterator; 029import java.util.List; 030 031import org.apache.commons.configuration2.Configuration; 032import org.apache.commons.lang3.StringUtils; 033import org.apache.logging.log4j.LogManager; 034import org.apache.logging.log4j.Logger; 035import org.apache.turbine.Turbine; 036import org.apache.turbine.pipeline.PipelineData; 037import org.apache.turbine.services.InitializationException; 038import org.apache.turbine.services.TurbineServices; 039import org.apache.turbine.services.pull.PullService; 040import org.apache.turbine.services.template.BaseTemplateEngineService; 041import org.apache.turbine.util.LocaleUtils; 042import org.apache.turbine.util.RunData; 043import org.apache.turbine.util.TurbineException; 044import org.apache.velocity.VelocityContext; 045import org.apache.velocity.app.VelocityEngine; 046import org.apache.velocity.app.event.EventCartridge; 047import org.apache.velocity.app.event.MethodExceptionEventHandler; 048import org.apache.velocity.context.Context; 049import org.apache.velocity.runtime.RuntimeConstants; 050import org.apache.velocity.util.introspection.Info; 051 052/** 053 * This is a Service that can process Velocity templates from within a 054 * Turbine Screen. It is used in conjunction with the templating service 055 * as a Templating Engine for templates ending in "vm". It registers 056 * itself as translation engine with the template service and gets 057 * accessed from there. After configuring it in your properties, it 058 * should never be necessary to call methods from this service directly. 059 * 060 * Here's an example of how you might use it from a 061 * screen:<br> 062 * 063 * <code> 064 * Context context = TurbineVelocity.getContext(data);<br> 065 * context.put("message", "Hello from Turbine!");<br> 066 * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br> 067 * data.getPage().getBody().addElement(results);<br> 068 * </code> 069 * 070 * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a> 071 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a> 072 * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a> 073 * @author <a href="mailto:sean@informage.ent">Sean Legassick</a> 074 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a> 075 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 076 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a> 077 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a> 078 * @version $Id: TurbineVelocityService.java 1854787 2019-03-04 18:30:25Z tv $ 079 */ 080public class TurbineVelocityService 081 extends BaseTemplateEngineService 082 implements VelocityService, 083 MethodExceptionEventHandler 084{ 085 /** The generic resource loader path property in velocity.*/ 086 private static final String RESOURCE_LOADER_PATH = ".resource.loader.path"; 087 088 /** The prefix used for URIs which are of type <code>jar</code>. */ 089 private static final String JAR_PREFIX = "jar:"; 090 091 /** The prefix used for URIs which are of type <code>absolute</code>. */ 092 private static final String ABSOLUTE_PREFIX = "file://"; 093 094 /** Logging */ 095 private static final Logger log = LogManager.getLogger(TurbineVelocityService.class); 096 097 /** Encoding used when reading the templates. */ 098 private String defaultInputEncoding; 099 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 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}