    001    package 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
    024    import java.io.ByteArrayOutputStream;
    025    import java.io.IOException;
    026    import java.io.OutputStream;
    027    import java.io.OutputStreamWriter;
    028    import java.io.Writer;
    029    import java.util.Iterator;
    030    import java.util.List;
    031
    032    import org.apache.commons.collections.ExtendedProperties;
    033    import org.apache.commons.configuration.Configuration;
    034    import org.apache.commons.lang.StringUtils;
    035    import org.apache.commons.logging.Log;
    036    import org.apache.commons.logging.LogFactory;
    037    import org.apache.turbine.Turbine;
    038    import org.apache.turbine.pipeline.PipelineData;
    039    import org.apache.turbine.services.InitializationException;
    040    import org.apache.turbine.services.pull.PullService;
    041    import org.apache.turbine.services.pull.TurbinePull;
    042    import org.apache.turbine.services.template.BaseTemplateEngineService;
    043    import org.apache.turbine.util.RunData;
    044    import org.apache.turbine.util.TurbineException;
    045    import org.apache.velocity.VelocityContext;
    046    import org.apache.velocity.app.Velocity;
    047    import org.apache.velocity.app.event.EventCartridge;
    048    import org.apache.velocity.app.event.MethodExceptionEventHandler;
    049    import org.apache.velocity.context.Context;
    050    import org.apache.velocity.runtime.log.Log4JLogChute;
    051
    052    /**
    053     * This is a Service that can process Velocity templates from within a
    054     * Turbine Screen. It is used in conjunction with the templating service
    055     * as a Templating Engine for templates ending in "vm". It registers
    056     * itself as translation engine with the template service and gets
    057     * accessed from there. After configuring it in your properties, it
    058     * should never be necessary to call methods from this service directly.
    059     *
    060     * Here's an example of how you might use it from a
    061     * screen:<br>
    062     *
    063     * <code>
    064     * Context context = TurbineVelocity.getContext(data);<br>
    065     * context.put("message", "Hello from Turbine!");<br>
    066     * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
    067     * data.getPage().getBody().addElement(results);<br>
    068     * </code>
    069     *
    070     * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
    071     * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
    072     * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
    073     * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
    074     * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
    075     * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
    076     * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
    077     * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
    078     * @version $Id: TurbineVelocityService.java 1073172 2011-02-21 22:16:51Z tv $
    079     */
    080    public class TurbineVelocityService
    081            extends BaseTemplateEngineService
    082            implements VelocityService,
    083                       MethodExceptionEventHandler
    084    {
    085        /** The generic resource loader path property in velocity.*/
    086        private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
    087
    088        /** Default character set to use if not specified in the RunData object. */
    089        private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
    090
    091        /** The prefix used for URIs which are of type <code>jar</code>. */
    092        private static final String JAR_PREFIX = "jar:";
    093
    094        /** The prefix used for URIs which are of type <code>absolute</code>. */
    095        private static final String ABSOLUTE_PREFIX = "file://";
    096
    097        /** Logging */
    098        private static Log log = LogFactory.getLog(TurbineVelocityService.class);
    099
    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    }