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.nio.charset.Charset; 029import java.util.Iterator; 030import java.util.List; 031 032import org.apache.commons.configuration2.Configuration; 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$ 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 Charset defaultInputEncoding; 099 100 /** Encoding used by the outputstream when handling the requests. */ 101 private Charset 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 String inputEncoding = getConfiguration().getString("input.encoding", LocaleUtils.getDefaultInputEncoding()); 147 defaultInputEncoding = Charset.forName(inputEncoding); 148 149 String outputEncodingString = getConfiguration().getString("output.encoding"); 150 if (outputEncodingString == null) 151 { 152 Charset outputEncoding = LocaleUtils.getOverrideCharset(); 153 if (outputEncoding == null) 154 { 155 outputEncoding = defaultInputEncoding; 156 } 157 158 defaultOutputEncoding = outputEncoding; 159 } 160 else 161 { 162 defaultOutputEncoding = Charset.forName(outputEncodingString); 163 } 164 165 setInit(true); 166 } 167 catch (Exception e) 168 { 169 throw new InitializationException( 170 "Failed to initialize TurbineVelocityService", e); 171 } 172 } 173 174 /** 175 * Create a Context object that also contains the globalContext. 176 * 177 * @return A Context object. 178 */ 179 @Override 180 public Context getContext() 181 { 182 Context globalContext = 183 pullModelActive ? pullService.getGlobalContext() : null; 184 185 Context ctx = new VelocityContext(globalContext); 186 return ctx; 187 } 188 189 /** 190 * This method returns a new, empty Context object. 191 * 192 * @return A Context Object. 193 */ 194 @Override 195 public Context getNewContext() 196 { 197 Context ctx = new VelocityContext(); 198 199 // Attach an Event Cartridge to it, so we get exceptions 200 // while invoking methods from the Velocity Screens 201 EventCartridge ec = new EventCartridge(); 202 ec.addEventHandler(this); 203 ec.attachToContext(ctx); 204 return ctx; 205 } 206 207 /** 208 * MethodException Event Cartridge handler 209 * for Velocity. 210 * 211 * It logs an exception thrown by the velocity processing 212 * on error level into the log file 213 * 214 * @param context The current context 215 * @param clazz The class that threw the exception 216 * @param method The Method name that threw the exception 217 * @param e The exception that would've been thrown 218 * @param info Information about the template, line and column the exception occurred 219 * @return A valid value to be used as Return value 220 */ 221 @Override 222 public Object methodException(Context context, @SuppressWarnings("rawtypes") Class clazz, String method, Exception e, Info info) 223 { 224 log.error("Class {}.{} threw Exception", clazz.getName(), method, e); 225 226 if (!catchErrors) 227 { 228 throw new RuntimeException(e); 229 } 230 231 return "[Turbine caught an Error in template " + info.getTemplateName() 232 + ", l:" + info.getLine() 233 + ", c:" + info.getColumn() 234 + ". Look into the turbine.log for further information]"; 235 } 236 237 /** 238 * Create a Context from the PipelineData object. Adds a pointer to 239 * the PipelineData object to the VelocityContext so that PipelineData 240 * is available in the templates. 241 * 242 * @param pipelineData The Turbine PipelineData object. 243 * @return A clone of the WebContext needed by Velocity. 244 */ 245 @Override 246 public Context getContext(PipelineData pipelineData) 247 { 248 //Map runDataMap = (Map)pipelineData.get(RunData.class); 249 RunData data = (RunData)pipelineData; 250 // Attempt to get it from the data first. If it doesn't 251 // exist, create it and then stuff it into the data. 252 Context context = (Context) 253 data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT); 254 255 if (context == null) 256 { 257 context = getContext(); 258 context.put(VelocityService.RUNDATA_KEY, data); 259 // we will add both data and pipelineData to the context. 260 context.put(VelocityService.PIPELINEDATA_KEY, pipelineData); 261 262 if (pullModelActive) 263 { 264 // Populate the toolbox with request scope, session scope 265 // and persistent scope tools (global tools are already in 266 // the toolBoxContent which has been wrapped to construct 267 // this request-specific context). 268 pullService.populateContext(context, pipelineData); 269 } 270 271 data.getTemplateInfo().setTemplateContext( 272 VelocityService.CONTEXT, context); 273 } 274 return context; 275 } 276 277 /** 278 * Process the request and fill in the template with the values 279 * you set in the Context. 280 * 281 * @param context The populated context. 282 * @param filename The file name of the template. 283 * @return The process template as a String. 284 * 285 * @throws TurbineException Any exception thrown while processing will be 286 * wrapped into a TurbineException and rethrown. 287 */ 288 @Override 289 public String handleRequest(Context context, String filename) 290 throws TurbineException 291 { 292 String results = null; 293 OutputStreamWriter writer = null; 294 Charset charset = getOutputCharSet(context); 295 296 try (ByteArrayOutputStream bytes = new ByteArrayOutputStream()) 297 { 298 writer = new OutputStreamWriter(bytes, charset); 299 300 executeRequest(context, filename, writer); 301 writer.flush(); 302 results = bytes.toString(charset.name()); 303 } 304 catch (Exception e) 305 { 306 renderingError(filename, e); 307 } 308 309 return results; 310 } 311 312 /** 313 * Process the request and fill in the template with the values 314 * you set in the Context. 315 * 316 * @param context A Context. 317 * @param filename A String with the filename of the template. 318 * @param output A OutputStream where we will write the process template as 319 * a String. 320 * 321 * @throws TurbineException Any exception thrown while processing will be 322 * wrapped into a TurbineException and rethrown. 323 */ 324 @Override 325 public void handleRequest(Context context, String filename, 326 OutputStream output) 327 throws TurbineException 328 { 329 Charset charset = getOutputCharSet(context); 330 331 try (OutputStreamWriter writer = new OutputStreamWriter(output, charset)) 332 { 333 executeRequest(context, filename, writer); 334 } 335 catch (Exception e) 336 { 337 renderingError(filename, e); 338 } 339 } 340 341 /** 342 * Process the request and fill in the template with the values 343 * you set in the Context. 344 * 345 * @param context A Context. 346 * @param filename A String with the filename of the template. 347 * @param writer A Writer where we will write the process template as 348 * a String. 349 * 350 * @throws TurbineException Any exception thrown while processing will be 351 * wrapped into a TurbineException and rethrown. 352 */ 353 @Override 354 public void handleRequest(Context context, String filename, Writer writer) 355 throws TurbineException 356 { 357 try 358 { 359 executeRequest(context, filename, writer); 360 } 361 catch (Exception e) 362 { 363 renderingError(filename, e); 364 } 365 finally 366 { 367 try 368 { 369 if (writer != null) 370 { 371 writer.flush(); 372 } 373 } 374 catch (Exception ignored) 375 { 376 // do nothing. 377 } 378 } 379 } 380 381 382 /** 383 * Process the request and fill in the template with the values 384 * you set in the Context. Apply the character and template 385 * encodings from RunData to the result. 386 * 387 * @param context A Context. 388 * @param filename A String with the filename of the template. 389 * @param writer A OutputStream where we will write the process template as 390 * a String. 391 * 392 * @throws Exception A problem occurred. 393 */ 394 private void executeRequest(Context context, String filename, 395 Writer writer) 396 throws Exception 397 { 398 Charset encoding = getTemplateEncoding(context); 399 400 if (encoding == null) 401 { 402 encoding = defaultOutputEncoding; 403 } 404 405 velocity.mergeTemplate(filename, encoding.name(), context, writer); 406 } 407 408 /** 409 * Retrieve the required charset from the Turbine RunData in the context 410 * 411 * @param context A Context. 412 * @return The character set applied to the resulting String. 413 */ 414 private Charset getOutputCharSet(Context context) 415 { 416 Charset charset = null; 417 418 Object data = context.get(VelocityService.RUNDATA_KEY); 419 if ((data != null) && (data instanceof RunData)) 420 { 421 charset = ((RunData) data).getCharset(); 422 } 423 424 return charset == null ? defaultOutputEncoding : charset; 425 } 426 427 /** 428 * Retrieve the required encoding from the Turbine RunData in the context 429 * 430 * @param context A Context. 431 * @return The encoding applied to the resulting String. 432 */ 433 private Charset getTemplateEncoding(Context context) 434 { 435 Charset encoding = null; 436 437 Object data = context.get(VelocityService.RUNDATA_KEY); 438 if ((data != null) && (data instanceof RunData) && (((RunData) data).getTemplateEncoding() != null) ) 439 { 440 encoding = Charset.forName(((RunData) data).getTemplateEncoding()); 441 } 442 443 return encoding != null ? encoding : defaultInputEncoding; 444 } 445 446 /** 447 * Macro to handle rendering errors. 448 * 449 * @param filename The file name of the unrenderable template. 450 * @param e The error. 451 * 452 * @throws TurbineException Thrown every time. Adds additional 453 * information to <code>e</code>. 454 */ 455 private static final void renderingError(String filename, Exception e) 456 throws TurbineException 457 { 458 String err = "Error rendering Velocity template: " + filename; 459 log.error(err, e); 460 throw new TurbineException(err, e); 461 } 462 463 /** 464 * Setup the velocity runtime by using a subset of the 465 * Turbine configuration which relates to velocity. 466 * 467 * @throws Exception An Error occurred. 468 * @return an initialized VelocityEngine instance 469 */ 470 private VelocityEngine getInitializedVelocityEngine() 471 throws Exception 472 { 473 // Get the configuration for this service. 474 Configuration conf = getConfiguration(); 475 476 catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT); 477 478 // backward compatibility, can be overridden in the configuration 479 conf.setProperty(RuntimeConstants.RUNTIME_LOG_NAME, "velocity"); 480 481 VelocityEngine velocity = new VelocityEngine(); 482 setVelocityProperties(velocity, conf); 483 velocity.init(); 484 485 return velocity; 486 } 487 488 489 /** 490 * This method generates the Properties object necessary 491 * for the initialization of Velocity. It also converts the various 492 * resource loader pathes into webapp relative pathes. It also 493 * 494 * @param velocity The Velocity engine 495 * @param conf The Velocity Service configuration 496 * 497 * @throws Exception If a problem occurred while converting the properties. 498 */ 499 500 protected void setVelocityProperties(VelocityEngine velocity, Configuration conf) 501 throws Exception 502 { 503 // Fix up all the template resource loader pathes to be 504 // webapp relative. Copy all other keys verbatim into the 505 // veloConfiguration. 506 507 for (Iterator<String> i = conf.getKeys(); i.hasNext();) 508 { 509 String key = i.next(); 510 if (!key.endsWith(RESOURCE_LOADER_PATH)) 511 { 512 Object value = conf.getProperty(key); 513 if (value instanceof List<?>) 514 { 515 for (Object name2 : ((List<?>) value)) 516 { 517 velocity.addProperty(key, name2); 518 } 519 } 520 else 521 { 522 velocity.addProperty(key, value); 523 } 524 log.debug("Adding {} -> {}", key, value); 525 continue; // for() 526 } 527 528 List<Object> paths = conf.getList(key, null); 529 if (paths == null) 530 { 531 // We don't copy this into VeloProperties, because 532 // null value is unhealthy for the ExtendedProperties object... 533 continue; // for() 534 } 535 536 // Translate the supplied pathes given here. 537 // the following three different kinds of 538 // pathes must be translated to be webapp-relative 539 // 540 // jar:file://path-component!/entry-component 541 // file://path-component 542 // path/component 543 for (Object p : paths) 544 { 545 String path = (String)p; 546 log.debug("Translating {}", path); 547 548 if (path.startsWith(JAR_PREFIX)) 549 { 550 // skip jar: -> 4 chars 551 if (path.substring(4).startsWith(ABSOLUTE_PREFIX)) 552 { 553 // We must convert up to the jar path separator 554 int jarSepIndex = path.indexOf("!/"); 555 556 // jar:file:// -> skip 11 chars 557 path = (jarSepIndex < 0) 558 ? Turbine.getRealPath(path.substring(11)) 559 // Add the path after the jar path separator again to the new url. 560 : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex)); 561 562 log.debug("Result (absolute jar path): {}", path); 563 } 564 } 565 else if(path.startsWith(ABSOLUTE_PREFIX)) 566 { 567 // skip file:// -> 7 chars 568 path = Turbine.getRealPath(path.substring(7)); 569 570 log.debug("Result (absolute URL Path): {}", path); 571 } 572 // Test if this might be some sort of URL that we haven't encountered yet. 573 else if(path.indexOf("://") < 0) 574 { 575 path = Turbine.getRealPath(path); 576 577 log.debug("Result (normal fs reference): {}", path); 578 } 579 580 log.debug("Adding {} -> {}", key, path); 581 // Re-Add this property to the configuration object 582 velocity.addProperty(key, path); 583 } 584 } 585 } 586 587 /** 588 * Find out if a given template exists. Velocity 589 * will do its own searching to determine whether 590 * a template exists or not. 591 * 592 * @param template String template to search for 593 * @return True if the template can be loaded by Velocity 594 */ 595 @Override 596 public boolean templateExists(String template) 597 { 598 return velocity.resourceExists(template); 599 } 600 601 /** 602 * Performs post-request actions (releases context 603 * tools back to the object pool). 604 * 605 * @param context a Velocity Context 606 */ 607 @Override 608 public void requestFinished(Context context) 609 { 610 if (pullModelActive) 611 { 612 pullService.releaseTools(context); 613 } 614 } 615}