View Javadoc

    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.Velocity;
    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.log.Log4JLogChute;
    51
    52  /**
    53   * This is a Service that can process Velocity templates from within a
    54   * Turbine Screen. It is used in conjunction with the templating service
    55   * as a Templating Engine for templates ending in "vm". It registers
    56   * itself as translation engine with the template service and gets
    57   * accessed from there. After configuring it in your properties, it
    58   * should never be necessary to call methods from this service directly.
    59   *
    60   * Here's an example of how you might use it from a
    61   * screen:<br>
    62   *
    63   * <code>
    64   * Context context = TurbineVelocity.getContext(data);<br>
    65   * context.put("message", "Hello from Turbine!");<br>
    66   * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
    67   * data.getPage().getBody().addElement(results);<br>
    68   * </code>
    69   *
    70   * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
    71   * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
    72   * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
    73   * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
    74   * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
    75   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
    76   * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
    77   * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
    78   * @version $Id: TurbineVelocityService.java 1073172 2011-02-21 22:16:51Z tv $
    79   */
    80  public class TurbineVelocityService
    81          extends BaseTemplateEngineService
    82          implements VelocityService,
    83                     MethodExceptionEventHandler
    84  {
    85      /** The generic resource loader path property in velocity.*/
    86      private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
    87
    88      /** Default character set to use if not specified in the RunData object. */
    89      private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
    90
    91      /** The prefix used for URIs which are of type <code>jar</code>. */
    92      private static final String JAR_PREFIX = "jar:";
    93
    94      /** The prefix used for URIs which are of type <code>absolute</code>. */
    95      private static final String ABSOLUTE_PREFIX = "file://";
    96
    97      /** Logging */
    98      private static Log log = LogFactory.getLog(TurbineVelocityService.class);
    99
    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 }