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.pull.PullService;
041import org.apache.turbine.services.pull.TurbinePull;
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 1695634 2015-08-13 00:35:47Z 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 (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}