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