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.IOException; 026import java.io.OutputStream; 027import java.io.OutputStreamWriter; 028import java.io.Writer; 029import java.util.Iterator; 030import java.util.List; 031 032import org.apache.commons.collections.ExtendedProperties; 033import org.apache.commons.configuration.Configuration; 034import org.apache.commons.lang.StringUtils; 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.apache.turbine.Turbine; 038import org.apache.turbine.pipeline.PipelineData; 039import org.apache.turbine.services.InitializationException; 040import org.apache.turbine.services.TurbineServices; 041import org.apache.turbine.services.pull.PullService; 042import org.apache.turbine.services.template.BaseTemplateEngineService; 043import org.apache.turbine.util.RunData; 044import org.apache.turbine.util.TurbineException; 045import org.apache.velocity.VelocityContext; 046import org.apache.velocity.app.VelocityEngine; 047import org.apache.velocity.app.event.EventCartridge; 048import org.apache.velocity.app.event.MethodExceptionEventHandler; 049import org.apache.velocity.context.Context; 050import org.apache.velocity.runtime.RuntimeConstants; 051import org.apache.velocity.runtime.log.CommonsLogLogChute; 052 053/** 054 * This is a Service that can process Velocity templates from within a 055 * Turbine Screen. It is used in conjunction with the templating service 056 * as a Templating Engine for templates ending in "vm". It registers 057 * itself as translation engine with the template service and gets 058 * accessed from there. After configuring it in your properties, it 059 * should never be necessary to call methods from this service directly. 060 * 061 * Here's an example of how you might use it from a 062 * screen:<br> 063 * 064 * <code> 065 * Context context = TurbineVelocity.getContext(data);<br> 066 * context.put("message", "Hello from Turbine!");<br> 067 * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br> 068 * data.getPage().getBody().addElement(results);<br> 069 * </code> 070 * 071 * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a> 072 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a> 073 * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a> 074 * @author <a href="mailto:sean@informage.ent">Sean Legassick</a> 075 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a> 076 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 077 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a> 078 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a> 079 * @version $Id: TurbineVelocityService.java 1773378 2016-12-09 13:19:59Z tv $ 080 */ 081public class TurbineVelocityService 082 extends BaseTemplateEngineService 083 implements VelocityService, 084 MethodExceptionEventHandler 085{ 086 /** The generic resource loader path property in velocity.*/ 087 private static final String RESOURCE_LOADER_PATH = ".resource.loader.path"; 088 089 /** Default character set to use if not specified in the RunData object. */ 090 private static final String DEFAULT_CHAR_SET = "ISO-8859-1"; 091 092 /** The prefix used for URIs which are of type <code>jar</code>. */ 093 private static final String JAR_PREFIX = "jar:"; 094 095 /** The prefix used for URIs which are of type <code>absolute</code>. */ 096 private static final String ABSOLUTE_PREFIX = "file://"; 097 098 /** Logging */ 099 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}