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}