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