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.pull.PullService; 041import org.apache.turbine.services.pull.TurbinePull; 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 1695634 2015-08-13 00:35:47Z 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 (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}