    001package org.apache.turbine.services.velocity;
    002
    003
    004/*
    005 * Licensed to the Apache Software Foundation (ASF) under one
    006 * or more contributor license agreements.  See the NOTICE file
    007 * distributed with this work for additional information
    008 * regarding copyright ownership.  The ASF licenses this file
    009 * to you under the Apache License, Version 2.0 (the
    010 * "License"); you may not use this file except in compliance
    011 * with the License.  You may obtain a copy of the License at
    012 *
    013 *   http://www.apache.org/licenses/LICENSE-2.0
    014 *
    015 * Unless required by applicable law or agreed to in writing,
    016 * software distributed under the License is distributed on an
    017 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    018 * KIND, either express or implied.  See the License for the
    019 * specific language governing permissions and limitations
    020 * under the License.
    021 */
    022
    023
    024import java.io.ByteArrayOutputStream;
    025import java.io.IOException;
    026import java.io.OutputStream;
    027import java.io.OutputStreamWriter;
    028import java.io.Writer;
    029import java.util.Iterator;
    030import java.util.List;
    031
    032import org.apache.commons.collections.ExtendedProperties;
    033import org.apache.commons.configuration.Configuration;
    034import org.apache.commons.lang.StringUtils;
    035import org.apache.commons.logging.Log;
    036import org.apache.commons.logging.LogFactory;
    037import org.apache.turbine.Turbine;
    038import org.apache.turbine.pipeline.PipelineData;
    039import org.apache.turbine.services.InitializationException;
    040import org.apache.turbine.services.TurbineServices;
    041import org.apache.turbine.services.pull.PullService;
    042import org.apache.turbine.services.template.BaseTemplateEngineService;
    043import org.apache.turbine.util.RunData;
    044import org.apache.turbine.util.TurbineException;
    045import org.apache.velocity.VelocityContext;
    046import org.apache.velocity.app.VelocityEngine;
    047import org.apache.velocity.app.event.EventCartridge;
    048import org.apache.velocity.app.event.MethodExceptionEventHandler;
    049import org.apache.velocity.context.Context;
    050import org.apache.velocity.runtime.RuntimeConstants;
    051import org.apache.velocity.runtime.log.CommonsLogLogChute;
    052
    053/**
    054 * This is a Service that can process Velocity templates from within a
    055 * Turbine Screen. It is used in conjunction with the templating service
    056 * as a Templating Engine for templates ending in "vm". It registers
    057 * itself as translation engine with the template service and gets
    058 * accessed from there. After configuring it in your properties, it
    059 * should never be necessary to call methods from this service directly.
    060 *
    061 * Here's an example of how you might use it from a
    062 * screen:<br>
    063 *
    064 * <code>
    065 * Context context = TurbineVelocity.getContext(data);<br>
    066 * context.put("message", "Hello from Turbine!");<br>
    067 * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
    068 * data.getPage().getBody().addElement(results);<br>
    069 * </code>
    070 *
    071 * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
    072 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
    073 * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
    074 * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
    075 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
    076 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
    077 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
    078 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
    079 * @version $Id: TurbineVelocityService.java 1773378 2016-12-09 13:19:59Z tv $
    080 */
    081public class TurbineVelocityService
    082        extends BaseTemplateEngineService
    083        implements VelocityService,
    084                   MethodExceptionEventHandler
    085{
    086    /** The generic resource loader path property in velocity.*/
    087    private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
    088
    089    /** Default character set to use if not specified in the RunData object. */
    090    private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
    091
    092    /** The prefix used for URIs which are of type <code>jar</code>. */
    093    private static final String JAR_PREFIX = "jar:";
    094
    095    /** The prefix used for URIs which are of type <code>absolute</code>. */
    096    private static final String ABSOLUTE_PREFIX = "file://";
    097
    098    /** Logging */
    099    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}