Coverage Report - org.apache.turbine.services.velocity.TurbineVelocityService
 
Classes in this File Line Coverage Branch Coverage Complexity
TurbineVelocityService
77%
112/145
61%
33/54
3,312
 
 1  
 package org.apache.turbine.services.velocity;
 2  
 
 3  
 
 4  
 /*
 5  
  * Licensed to the Apache Software Foundation (ASF) under one
 6  
  * or more contributor license agreements.  See the NOTICE file
 7  
  * distributed with this work for additional information
 8  
  * regarding copyright ownership.  The ASF licenses this file
 9  
  * to you under the Apache License, Version 2.0 (the
 10  
  * "License"); you may not use this file except in compliance
 11  
  * with the License.  You may obtain a copy of the License at
 12  
  *
 13  
  *   http://www.apache.org/licenses/LICENSE-2.0
 14  
  *
 15  
  * Unless required by applicable law or agreed to in writing,
 16  
  * software distributed under the License is distributed on an
 17  
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 18  
  * KIND, either express or implied.  See the License for the
 19  
  * specific language governing permissions and limitations
 20  
  * under the License.
 21  
  */
 22  
 
 23  
 
 24  
 import java.io.ByteArrayOutputStream;
 25  
 import java.io.IOException;
 26  
 import java.io.OutputStream;
 27  
 import java.io.OutputStreamWriter;
 28  
 import java.io.Writer;
 29  
 import java.util.Iterator;
 30  
 import java.util.List;
 31  
 
 32  
 import org.apache.commons.collections.ExtendedProperties;
 33  
 import org.apache.commons.configuration.Configuration;
 34  
 import org.apache.commons.lang.StringUtils;
 35  
 import org.apache.commons.logging.Log;
 36  
 import org.apache.commons.logging.LogFactory;
 37  
 import org.apache.turbine.Turbine;
 38  
 import org.apache.turbine.pipeline.PipelineData;
 39  
 import org.apache.turbine.services.InitializationException;
 40  
 import org.apache.turbine.services.pull.PullService;
 41  
 import org.apache.turbine.services.pull.TurbinePull;
 42  
 import org.apache.turbine.services.template.BaseTemplateEngineService;
 43  
 import org.apache.turbine.util.RunData;
 44  
 import org.apache.turbine.util.TurbineException;
 45  
 import org.apache.velocity.VelocityContext;
 46  
 import org.apache.velocity.app.VelocityEngine;
 47  
 import org.apache.velocity.app.event.EventCartridge;
 48  
 import org.apache.velocity.app.event.MethodExceptionEventHandler;
 49  
 import org.apache.velocity.context.Context;
 50  
 import org.apache.velocity.runtime.RuntimeConstants;
 51  
 import org.apache.velocity.runtime.log.CommonsLogLogChute;
 52  
 
 53  
 /**
 54  
  * This is a Service that can process Velocity templates from within a
 55  
  * Turbine Screen. It is used in conjunction with the templating service
 56  
  * as a Templating Engine for templates ending in "vm". It registers
 57  
  * itself as translation engine with the template service and gets
 58  
  * accessed from there. After configuring it in your properties, it
 59  
  * should never be necessary to call methods from this service directly.
 60  
  *
 61  
  * Here's an example of how you might use it from a
 62  
  * screen:<br>
 63  
  *
 64  
  * <code>
 65  
  * Context context = TurbineVelocity.getContext(data);<br>
 66  
  * context.put("message", "Hello from Turbine!");<br>
 67  
  * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
 68  
  * data.getPage().getBody().addElement(results);<br>
 69  
  * </code>
 70  
  *
 71  
  * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
 72  
  * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
 73  
  * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
 74  
  * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
 75  
  * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
 76  
  * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
 77  
  * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
 78  
  * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
 79  
  * @version $Id: TurbineVelocityService.java 1695634 2015-08-13 00:35:47Z tv $
 80  
  */
 81  22
 public class TurbineVelocityService
 82  
         extends BaseTemplateEngineService
 83  
         implements VelocityService,
 84  
                    MethodExceptionEventHandler
 85  
 {
 86  
     /** The generic resource loader path property in velocity.*/
 87  
     private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
 88  
 
 89  
     /** Default character set to use if not specified in the RunData object. */
 90  
     private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
 91  
 
 92  
     /** The prefix used for URIs which are of type <code>jar</code>. */
 93  
     private static final String JAR_PREFIX = "jar:";
 94  
 
 95  
     /** The prefix used for URIs which are of type <code>absolute</code>. */
 96  
     private static final String ABSOLUTE_PREFIX = "file://";
 97  
 
 98  
     /** Logging */
 99  22
     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  22
     private boolean pullModelActive = false;
 109  
 
 110  
     /** Shall we catch Velocity Errors and report them in the log file? */
 111  22
     private boolean catchErrors = true;
 112  
 
 113  
     /** Velocity runtime instance */
 114  22
     private VelocityEngine velocity = null;
 115  
 
 116  
     /** Internal Reference to the pull Service */
 117  22
     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  26
             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  26
             if (TurbinePull.isRegistered())
 140  
             {
 141  26
                 pullModelActive = true;
 142  
 
 143  26
                 pullService = TurbinePull.getService();
 144  
 
 145  26
                 log.debug("Activated Pull Tools");
 146  
             }
 147  
 
 148  
             // Register with the template service.
 149  26
             registerConfiguration(VelocityService.VELOCITY_EXTENSION);
 150  
 
 151  26
             defaultInputEncoding = getConfiguration().getString("input.encoding", DEFAULT_CHAR_SET);
 152  26
             defaultOutputEncoding = getConfiguration().getString("output.encoding", defaultInputEncoding);
 153  
 
 154  26
             setInit(true);
 155  
         }
 156  0
         catch (Exception e)
 157  
         {
 158  0
             throw new InitializationException(
 159  
                 "Failed to initialize TurbineVelocityService", e);
 160  26
         }
 161  26
     }
 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  9
         Context globalContext =
 172  
                 pullModelActive ? pullService.getGlobalContext() : null;
 173  
 
 174  9
         Context ctx = new VelocityContext(globalContext);
 175  9
         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  26
         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  26
         EventCartridge ec = new EventCartridge();
 191  26
         ec.addEventHandler(this);
 192  26
         ec.attachToContext(ctx);
 193  26
         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  0
         log.error("Class " + clazz.getName() + "." + method + " threw Exception", e);
 215  
 
 216  0
         if (!catchErrors)
 217  
         {
 218  0
             throw e;
 219  
         }
 220  
 
 221  0
         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  28
         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  25
         Context context = (Context)
 240  
             data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
 241  
 
 242  25
         if (context == null)
 243  
         {
 244  9
             context = getContext();
 245  9
             context.put(VelocityService.RUNDATA_KEY, data);
 246  
             // we will add both data and pipelineData to the context.
 247  9
             context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
 248  
 
 249  9
             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  9
                 pullService.populateContext(context, pipelineData);
 256  
             }
 257  
 
 258  9
             data.getTemplateInfo().setTemplateContext(
 259  
                 VelocityService.CONTEXT, context);
 260  
         }
 261  25
         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  2
         String results = null;
 280  2
         ByteArrayOutputStream bytes = null;
 281  2
         OutputStreamWriter writer = null;
 282  2
         String charset = getOutputCharSet(context);
 283  
 
 284  
         try
 285  
         {
 286  2
             bytes = new ByteArrayOutputStream();
 287  
 
 288  2
             writer = new OutputStreamWriter(bytes, charset);
 289  
 
 290  2
             executeRequest(context, filename, writer);
 291  2
             writer.flush();
 292  2
             results = bytes.toString(charset);
 293  
         }
 294  0
         catch (Exception e)
 295  
         {
 296  0
             renderingError(filename, e);
 297  
         }
 298  
         finally
 299  
         {
 300  0
             try
 301  
             {
 302  2
                 if (bytes != null)
 303  
                 {
 304  2
                     bytes.close();
 305  
                 }
 306  
             }
 307  0
             catch (IOException ignored)
 308  
             {
 309  
                 // do nothing.
 310  2
             }
 311  0
         }
 312  2
         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  2
         String charset  = getOutputCharSet(context);
 333  2
         OutputStreamWriter writer = null;
 334  
 
 335  
         try
 336  
         {
 337  2
             writer = new OutputStreamWriter(output, charset);
 338  2
             executeRequest(context, filename, writer);
 339  
         }
 340  0
         catch (Exception e)
 341  
         {
 342  0
             renderingError(filename, e);
 343  
         }
 344  
         finally
 345  
         {
 346  0
             try
 347  
             {
 348  2
                 if (writer != null)
 349  
                 {
 350  2
                     writer.flush();
 351  
                 }
 352  
             }
 353  0
             catch (Exception ignored)
 354  
             {
 355  
                 // do nothing.
 356  2
             }
 357  0
         }
 358  2
     }
 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  0
             executeRequest(context, filename, writer);
 380  
         }
 381  0
         catch (Exception e)
 382  
         {
 383  0
             renderingError(filename, e);
 384  
         }
 385  
         finally
 386  
         {
 387  0
             try
 388  
             {
 389  0
                 if (writer != null)
 390  
                 {
 391  0
                     writer.flush();
 392  
                 }
 393  
             }
 394  0
             catch (Exception ignored)
 395  
             {
 396  
                 // do nothing.
 397  0
             }
 398  0
         }
 399  0
     }
 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  4
         String encoding = getTemplateEncoding(context);
 419  
 
 420  4
         if (encoding == null)
 421  
         {
 422  0
           encoding = defaultOutputEncoding;
 423  
         }
 424  
 
 425  4
                 velocity.mergeTemplate(filename, encoding, context, writer);
 426  4
     }
 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  4
         String charset = null;
 437  
 
 438  4
         Object data = context.get(VelocityService.RUNDATA_KEY);
 439  4
         if ((data != null) && (data instanceof RunData))
 440  
         {
 441  4
             charset = ((RunData) data).getCharSet();
 442  
         }
 443  
 
 444  4
         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  4
         String encoding = null;
 456  
 
 457  4
         Object data = context.get(VelocityService.RUNDATA_KEY);
 458  4
         if ((data != null) && (data instanceof RunData))
 459  
         {
 460  4
             encoding = ((RunData) data).getTemplateEncoding();
 461  
         }
 462  
 
 463  4
         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  0
         String err = "Error rendering Velocity template: " + filename;
 479  0
         log.error(err, e);
 480  0
         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  26
         Configuration conf = getConfiguration();
 494  
 
 495  26
         catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
 496  
 
 497  26
         conf.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
 498  
                 CommonsLogLogChute.class.getName());
 499  26
         conf.setProperty(CommonsLogLogChute.LOGCHUTE_COMMONS_LOG_NAME,
 500  
                 "velocity");
 501  
 
 502  26
         velocity = new VelocityEngine();
 503  26
         velocity.setExtendedProperties(createVelocityProperties(conf));
 504  26
         velocity.init();
 505  26
     }
 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  27
         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  27
         for (Iterator<String> i = conf.getKeys(); i.hasNext();)
 533  
         {
 534  512
             String key = i.next();
 535  512
             if (!key.endsWith(RESOURCE_LOADER_PATH))
 536  
             {
 537  405
                 Object value = conf.getProperty(key);
 538  405
                 if (value instanceof List<?>) {
 539  0
                     for (Iterator<?> itr = ((List<?>)value).iterator(); itr.hasNext();)
 540  
                     {
 541  0
                         veloConfig.addProperty(key, itr.next());
 542  
                     }
 543  
                 }
 544  
                 else
 545  
                 {
 546  405
                     veloConfig.addProperty(key, value);
 547  
                 }
 548  405
                 continue; // for()
 549  
             }
 550  
 
 551  107
             List<Object> paths = conf.getList(key, null);
 552  107
             if (paths == null)
 553  
             {
 554  
                 // We don't copy this into VeloProperties, because
 555  
                 // null value is unhealthy for the ExtendedProperties object...
 556  0
                 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  107
             for (Object p : paths)
 567  
             {
 568  107
                     String path = (String)p;
 569  107
                 log.debug("Translating " + path);
 570  
 
 571  107
                 if (path.startsWith(JAR_PREFIX))
 572  
                 {
 573  
                     // skip jar: -> 4 chars
 574  40
                     if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
 575  
                     {
 576  
                         // We must convert up to the jar path separator
 577  30
                         int jarSepIndex = path.indexOf("!/");
 578  
 
 579  
                         // jar:file:// -> skip 11 chars
 580  30
                         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  30
                         log.debug("Result (absolute jar path): " + path);
 586  30
                     }
 587  
                 }
 588  67
                 else if(path.startsWith(ABSOLUTE_PREFIX))
 589  
                 {
 590  
                     // skip file:// -> 7 chars
 591  10
                     path = Turbine.getRealPath(path.substring(7));
 592  
 
 593  10
                     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  57
                 else if(path.indexOf("://") < 0)
 597  
                 {
 598  47
                     path = Turbine.getRealPath(path);
 599  
 
 600  47
                     log.debug("Result (normal fs reference): " + path);
 601  
                 }
 602  
 
 603  107
                 log.debug("Adding " + key + " -> " + path);
 604  
                 // Re-Add this property to the configuration object
 605  107
                 veloConfig.addProperty(key, path);
 606  107
             }
 607  107
         }
 608  27
         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  37
         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  2
         if (pullModelActive)
 635  
         {
 636  2
             pullService.releaseTools(context);
 637  
         }
 638  2
     }
 639  
 }