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.OutputStream;
026import java.io.OutputStreamWriter;
027import java.io.Writer;
028import java.util.Iterator;
029import java.util.List;
030
031import org.apache.commons.configuration2.Configuration;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.logging.log4j.LogManager;
034import org.apache.logging.log4j.Logger;
035import org.apache.turbine.Turbine;
036import org.apache.turbine.pipeline.PipelineData;
037import org.apache.turbine.services.InitializationException;
038import org.apache.turbine.services.TurbineServices;
039import org.apache.turbine.services.pull.PullService;
040import org.apache.turbine.services.template.BaseTemplateEngineService;
041import org.apache.turbine.util.LocaleUtils;
042import org.apache.turbine.util.RunData;
043import org.apache.turbine.util.TurbineException;
044import org.apache.velocity.VelocityContext;
045import org.apache.velocity.app.VelocityEngine;
046import org.apache.velocity.app.event.EventCartridge;
047import org.apache.velocity.app.event.MethodExceptionEventHandler;
048import org.apache.velocity.context.Context;
049import org.apache.velocity.runtime.RuntimeConstants;
050import org.apache.velocity.util.introspection.Info;
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 1854787 2019-03-04 18:30:25Z tv $
079 */
080public 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    /** The prefix used for URIs which are of type <code>jar</code>. */
089    private static final String JAR_PREFIX = "jar:";
090
091    /** The prefix used for URIs which are of type <code>absolute</code>. */
092    private static final String ABSOLUTE_PREFIX = "file://";
093
094    /** Logging */
095    private static final Logger log = LogManager.getLogger(TurbineVelocityService.class);
096
097    /** Encoding used when reading the templates. */
098    private String defaultInputEncoding;
099
100    /** Encoding used by the outputstream when handling the requests. */
101    private String defaultOutputEncoding;
102
103    /** Is the pullModelActive? */
104    private boolean pullModelActive = false;
105
106    /** Shall we catch Velocity Errors and report them in the log file? */
107    private boolean catchErrors = true;
108
109    /** Velocity runtime instance */
110    private VelocityEngine velocity = null;
111
112    /** Internal Reference to the pull Service */
113    private PullService pullService = null;
114
115
116    /**
117     * Load all configured components and initialize them. This is
118     * a zero parameter variant which queries the Turbine Servlet
119     * for its config.
120     *
121     * @throws InitializationException Something went wrong in the init
122     *         stage
123     */
124    @Override
125    public void init()
126            throws InitializationException
127    {
128        try
129        {
130            velocity = getInitializedVelocityEngine();
131
132            // We can only load the Pull Model ToolBox
133            // if the Pull service has been listed in the TR.props
134            // and the service has successfully been initialized.
135            if (TurbineServices.getInstance().isRegistered(PullService.SERVICE_NAME))
136            {
137                pullModelActive = true;
138                pullService = (PullService)TurbineServices.getInstance().getService(PullService.SERVICE_NAME);
139
140                log.debug("Activated Pull Tools");
141            }
142
143            // Register with the template service.
144            registerConfiguration(VelocityService.VELOCITY_EXTENSION);
145
146            defaultInputEncoding = getConfiguration().getString("input.encoding", LocaleUtils.getDefaultInputEncoding());
147
148            String outputEncoding = LocaleUtils.getOverrideCharSet();
149            if (outputEncoding == null)
150            {
151                outputEncoding = defaultInputEncoding;
152            }
153            defaultOutputEncoding = getConfiguration().getString("output.encoding", defaultInputEncoding);
154
155            setInit(true);
156        }
157        catch (Exception e)
158        {
159            throw new InitializationException(
160                "Failed to initialize TurbineVelocityService", e);
161        }
162    }
163
164    /**
165     * Create a Context object that also contains the globalContext.
166     *
167     * @return A Context object.
168     */
169    @Override
170    public Context getContext()
171    {
172        Context globalContext =
173                pullModelActive ? pullService.getGlobalContext() : null;
174
175        Context ctx = new VelocityContext(globalContext);
176        return ctx;
177    }
178
179    /**
180     * This method returns a new, empty Context object.
181     *
182     * @return A Context Object.
183     */
184    @Override
185    public Context getNewContext()
186    {
187        Context ctx = new VelocityContext();
188
189        // Attach an Event Cartridge to it, so we get exceptions
190        // while invoking methods from the Velocity Screens
191        EventCartridge ec = new EventCartridge();
192        ec.addEventHandler(this);
193        ec.attachToContext(ctx);
194        return ctx;
195    }
196
197    /**
198     * MethodException Event Cartridge handler
199     * for Velocity.
200     *
201     * It logs an exception thrown by the velocity processing
202     * on error level into the log file
203     *
204     * @param context The current context
205     * @param clazz The class that threw the exception
206     * @param method The Method name that threw the exception
207     * @param e The exception that would've been thrown
208     * @param info Information about the template, line and column the exception occurred
209     * @return A valid value to be used as Return value
210     */
211    @Override
212        public Object methodException(Context context, @SuppressWarnings("rawtypes") Class clazz, String method, Exception e, Info info)
213    {
214        log.error("Class {}.{} threw Exception", clazz.getName(), method, e);
215
216        if (!catchErrors)
217        {
218            throw new RuntimeException(e);
219        }
220
221        return "[Turbine caught an Error in template " + info.getTemplateName()
222            + ", l:" + info.getLine()
223            + ", c:" + info.getColumn()
224            + ". Look into the turbine.log for further information]";
225    }
226
227    /**
228     * Create a Context from the PipelineData object.  Adds a pointer to
229     * the PipelineData object to the VelocityContext so that PipelineData
230     * is available in the templates.
231     *
232     * @param pipelineData The Turbine PipelineData object.
233     * @return A clone of the WebContext needed by Velocity.
234     */
235    @Override
236    public Context getContext(PipelineData pipelineData)
237    {
238        //Map runDataMap = (Map)pipelineData.get(RunData.class);
239        RunData data = (RunData)pipelineData;
240        // Attempt to get it from the data first.  If it doesn't
241        // exist, create it and then stuff it into the data.
242        Context context = (Context)
243            data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
244
245        if (context == null)
246        {
247            context = getContext();
248            context.put(VelocityService.RUNDATA_KEY, data);
249            // we will add both data and pipelineData to the context.
250            context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
251
252            if (pullModelActive)
253            {
254                // Populate the toolbox with request scope, session scope
255                // and persistent scope tools (global tools are already in
256                // the toolBoxContent which has been wrapped to construct
257                // this request-specific context).
258                pullService.populateContext(context, pipelineData);
259            }
260
261            data.getTemplateInfo().setTemplateContext(
262                VelocityService.CONTEXT, context);
263        }
264        return context;
265    }
266
267    /**
268     * Process the request and fill in the template with the values
269     * you set in the Context.
270     *
271     * @param context  The populated context.
272     * @param filename The file name of the template.
273     * @return The process template as a String.
274     *
275     * @throws TurbineException Any exception thrown while processing will be
276     *         wrapped into a TurbineException and rethrown.
277     */
278    @Override
279    public String handleRequest(Context context, String filename)
280        throws TurbineException
281    {
282        String results = null;
283        OutputStreamWriter writer = null;
284        String charset = getOutputCharSet(context);
285
286        try (ByteArrayOutputStream 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
299        return results;
300    }
301
302    /**
303     * Process the request and fill in the template with the values
304     * you set in the Context.
305     *
306     * @param context A Context.
307     * @param filename A String with the filename of the template.
308     * @param output A OutputStream where we will write the process template as
309     * a String.
310     *
311     * @throws TurbineException Any exception thrown while processing will be
312     *         wrapped into a TurbineException and rethrown.
313     */
314    @Override
315    public void handleRequest(Context context, String filename,
316                              OutputStream output)
317            throws TurbineException
318    {
319        String charset  = getOutputCharSet(context);
320
321        try (OutputStreamWriter writer = new OutputStreamWriter(output, charset))
322        {
323            executeRequest(context, filename, writer);
324        }
325        catch (Exception e)
326        {
327            renderingError(filename, e);
328        }
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 writer A Writer where we will write the process template as
338     * a String.
339     *
340     * @throws TurbineException Any exception thrown while processing will be
341     *         wrapped into a TurbineException and rethrown.
342     */
343    @Override
344    public void handleRequest(Context context, String filename, Writer writer)
345            throws TurbineException
346    {
347        try
348        {
349            executeRequest(context, filename, writer);
350        }
351        catch (Exception e)
352        {
353            renderingError(filename, e);
354        }
355        finally
356        {
357            try
358            {
359                if (writer != null)
360                {
361                    writer.flush();
362                }
363            }
364            catch (Exception ignored)
365            {
366                // do nothing.
367            }
368        }
369    }
370
371
372    /**
373     * Process the request and fill in the template with the values
374     * you set in the Context. Apply the character and template
375     * encodings from RunData to the result.
376     *
377     * @param context A Context.
378     * @param filename A String with the filename of the template.
379     * @param writer A OutputStream where we will write the process template as
380     * a String.
381     *
382     * @throws Exception A problem occurred.
383     */
384    private void executeRequest(Context context, String filename,
385                                Writer writer)
386            throws Exception
387    {
388        String encoding = getTemplateEncoding(context);
389
390        if (encoding == null)
391        {
392          encoding = defaultOutputEncoding;
393        }
394
395                velocity.mergeTemplate(filename, encoding, context, writer);
396    }
397
398    /**
399     * Retrieve the required charset from the Turbine RunData in the context
400     *
401     * @param context A Context.
402     * @return The character set applied to the resulting String.
403     */
404    private String getOutputCharSet(Context context)
405    {
406        String charset = null;
407
408        Object data = context.get(VelocityService.RUNDATA_KEY);
409        if ((data != null) && (data instanceof RunData))
410        {
411            charset = ((RunData) data).getCharSet();
412        }
413
414        return (StringUtils.isEmpty(charset)) ? defaultOutputEncoding : charset;
415    }
416
417    /**
418     * Retrieve the required encoding from the Turbine RunData in the context
419     *
420     * @param context A Context.
421     * @return The encoding applied to the resulting String.
422     */
423    private String getTemplateEncoding(Context context)
424    {
425        String encoding = null;
426
427        Object data = context.get(VelocityService.RUNDATA_KEY);
428        if ((data != null) && (data instanceof RunData))
429        {
430            encoding = ((RunData) data).getTemplateEncoding();
431        }
432
433        return encoding != null ? encoding : defaultInputEncoding;
434    }
435
436    /**
437     * Macro to handle rendering errors.
438     *
439     * @param filename The file name of the unrenderable template.
440     * @param e        The error.
441     *
442     * @throws TurbineException Thrown every time.  Adds additional
443     *                             information to <code>e</code>.
444     */
445    private static final void renderingError(String filename, Exception e)
446            throws TurbineException
447    {
448        String err = "Error rendering Velocity template: " + filename;
449        log.error(err, e);
450        throw new TurbineException(err, e);
451    }
452
453    /**
454     * Setup the velocity runtime by using a subset of the
455     * Turbine configuration which relates to velocity.
456     *
457     * @throws Exception An Error occurred.
458     * @return an initialized VelocityEngine instance
459     */
460    private VelocityEngine getInitializedVelocityEngine()
461        throws Exception
462    {
463        // Get the configuration for this service.
464        Configuration conf = getConfiguration();
465
466        catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
467
468        // backward compatibility, can be overridden in the configuration
469        conf.setProperty(RuntimeConstants.RUNTIME_LOG_NAME, "velocity");
470
471        VelocityEngine velocity = new VelocityEngine();
472        setVelocityProperties(velocity, conf);
473        velocity.init();
474
475        return velocity;
476    }
477
478
479    /**
480     * This method generates the Properties object necessary
481     * for the initialization of Velocity. It also converts the various
482     * resource loader pathes into webapp relative pathes. It also
483     *
484     * @param velocity The Velocity engine
485     * @param conf The Velocity Service configuration
486     *
487     * @throws Exception If a problem occurred while converting the properties.
488     */
489
490    protected void setVelocityProperties(VelocityEngine velocity, Configuration conf)
491            throws Exception
492    {
493        // Fix up all the template resource loader pathes to be
494        // webapp relative. Copy all other keys verbatim into the
495        // veloConfiguration.
496
497        for (Iterator<String> i = conf.getKeys(); i.hasNext();)
498        {
499            String key = i.next();
500            if (!key.endsWith(RESOURCE_LOADER_PATH))
501            {
502                Object value = conf.getProperty(key);
503                if (value instanceof List<?>)
504                {
505                    for (Iterator<?> itr = ((List<?>)value).iterator(); itr.hasNext();)
506                    {
507                        velocity.addProperty(key, itr.next());
508                    }
509                }
510                else
511                {
512                    velocity.addProperty(key, value);
513                }
514                continue; // for()
515            }
516
517            List<Object> paths = conf.getList(key, null);
518            if (paths == null)
519            {
520                // We don't copy this into VeloProperties, because
521                // null value is unhealthy for the ExtendedProperties object...
522                continue; // for()
523            }
524
525            // Translate the supplied pathes given here.
526            // the following three different kinds of
527            // pathes must be translated to be webapp-relative
528            //
529            // jar:file://path-component!/entry-component
530            // file://path-component
531            // path/component
532            for (Object p : paths)
533            {
534                String path = (String)p;
535                log.debug("Translating {}", path);
536
537                if (path.startsWith(JAR_PREFIX))
538                {
539                    // skip jar: -> 4 chars
540                    if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
541                    {
542                        // We must convert up to the jar path separator
543                        int jarSepIndex = path.indexOf("!/");
544
545                        // jar:file:// -> skip 11 chars
546                        path = (jarSepIndex < 0)
547                            ? Turbine.getRealPath(path.substring(11))
548                        // Add the path after the jar path separator again to the new url.
549                            : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
550
551                        log.debug("Result (absolute jar path): {}", path);
552                    }
553                }
554                else if(path.startsWith(ABSOLUTE_PREFIX))
555                {
556                    // skip file:// -> 7 chars
557                    path = Turbine.getRealPath(path.substring(7));
558
559                    log.debug("Result (absolute URL Path): {}", path);
560                }
561                // Test if this might be some sort of URL that we haven't encountered yet.
562                else if(path.indexOf("://") < 0)
563                {
564                    path = Turbine.getRealPath(path);
565
566                    log.debug("Result (normal fs reference): {}", path);
567                }
568
569                log.debug("Adding {} -> {}", key, path);
570                // Re-Add this property to the configuration object
571                velocity.addProperty(key, path);
572            }
573        }
574    }
575
576    /**
577     * Find out if a given template exists. Velocity
578     * will do its own searching to determine whether
579     * a template exists or not.
580     *
581     * @param template String template to search for
582     * @return True if the template can be loaded by Velocity
583     */
584    @Override
585    public boolean templateExists(String template)
586    {
587        return velocity.resourceExists(template);
588    }
589
590    /**
591     * Performs post-request actions (releases context
592     * tools back to the object pool).
593     *
594     * @param context a Velocity Context
595     */
596    @Override
597    public void requestFinished(Context context)
598    {
599        if (pullModelActive)
600        {
601            pullService.releaseTools(context);
602        }
603    }
604}