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