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