001    package org.apache.turbine.services.velocity;
002    
003    
004    /*
005     * Licensed to the Apache Software Foundation (ASF) under one
006     * or more contributor license agreements.  See the NOTICE file
007     * distributed with this work for additional information
008     * regarding copyright ownership.  The ASF licenses this file
009     * to you under the Apache License, Version 2.0 (the
010     * "License"); you may not use this file except in compliance
011     * with the License.  You may obtain a copy of the License at
012     *
013     *   http://www.apache.org/licenses/LICENSE-2.0
014     *
015     * Unless required by applicable law or agreed to in writing,
016     * software distributed under the License is distributed on an
017     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
018     * KIND, either express or implied.  See the License for the
019     * specific language governing permissions and limitations
020     * under the License.
021     */
022    
023    
024    import java.io.ByteArrayOutputStream;
025    import java.io.IOException;
026    import java.io.OutputStream;
027    import java.io.OutputStreamWriter;
028    import java.io.Writer;
029    import java.util.Iterator;
030    import java.util.List;
031    
032    import org.apache.commons.collections.ExtendedProperties;
033    import org.apache.commons.configuration.Configuration;
034    import org.apache.commons.lang.StringUtils;
035    import org.apache.commons.logging.Log;
036    import org.apache.commons.logging.LogFactory;
037    import org.apache.turbine.Turbine;
038    import org.apache.turbine.pipeline.PipelineData;
039    import org.apache.turbine.services.InitializationException;
040    import org.apache.turbine.services.pull.PullService;
041    import org.apache.turbine.services.pull.TurbinePull;
042    import org.apache.turbine.services.template.BaseTemplateEngineService;
043    import org.apache.turbine.util.RunData;
044    import org.apache.turbine.util.TurbineException;
045    import org.apache.velocity.VelocityContext;
046    import org.apache.velocity.app.Velocity;
047    import org.apache.velocity.app.event.EventCartridge;
048    import org.apache.velocity.app.event.MethodExceptionEventHandler;
049    import org.apache.velocity.context.Context;
050    import org.apache.velocity.runtime.log.Log4JLogChute;
051    
052    /**
053     * This is a Service that can process Velocity templates from within a
054     * Turbine Screen. It is used in conjunction with the templating service
055     * as a Templating Engine for templates ending in "vm". It registers
056     * itself as translation engine with the template service and gets
057     * accessed from there. After configuring it in your properties, it
058     * should never be necessary to call methods from this service directly.
059     *
060     * Here's an example of how you might use it from a
061     * screen:<br>
062     *
063     * <code>
064     * Context context = TurbineVelocity.getContext(data);<br>
065     * context.put("message", "Hello from Turbine!");<br>
066     * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
067     * data.getPage().getBody().addElement(results);<br>
068     * </code>
069     *
070     * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
071     * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
072     * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
073     * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
074     * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
075     * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
076     * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
077     * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
078     * @version $Id: TurbineVelocityService.java 1073172 2011-02-21 22:16:51Z tv $
079     */
080    public class TurbineVelocityService
081            extends BaseTemplateEngineService
082            implements VelocityService,
083                       MethodExceptionEventHandler
084    {
085        /** The generic resource loader path property in velocity.*/
086        private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
087    
088        /** Default character set to use if not specified in the RunData object. */
089        private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
090    
091        /** The prefix used for URIs which are of type <code>jar</code>. */
092        private static final String JAR_PREFIX = "jar:";
093    
094        /** The prefix used for URIs which are of type <code>absolute</code>. */
095        private static final String ABSOLUTE_PREFIX = "file://";
096    
097        /** Logging */
098        private static Log log = LogFactory.getLog(TurbineVelocityService.class);
099    
100        /** Is the pullModelActive? */
101        private boolean pullModelActive = false;
102    
103        /** Shall we catch Velocity Errors and report them in the log file? */
104        private boolean catchErrors = true;
105    
106        /** Internal Reference to the pull Service */
107        private PullService pullService = null;
108    
109    
110        /**
111         * Load all configured components and initialize them. This is
112         * a zero parameter variant which queries the Turbine Servlet
113         * for its config.
114         *
115         * @throws InitializationException Something went wrong in the init
116         *         stage
117         */
118        @Override
119        public void init()
120                throws InitializationException
121        {
122            try
123            {
124                initVelocity();
125    
126                // We can only load the Pull Model ToolBox
127                // if the Pull service has been listed in the TR.props
128                // and the service has successfully been initialized.
129                if (TurbinePull.isRegistered())
130                {
131                    pullModelActive = true;
132    
133                    pullService = TurbinePull.getService();
134    
135                    log.debug("Activated Pull Tools");
136                }
137    
138                // Register with the template service.
139                registerConfiguration(VelocityService.VELOCITY_EXTENSION);
140    
141                setInit(true);
142            }
143            catch (Exception e)
144            {
145                throw new InitializationException(
146                    "Failed to initialize TurbineVelocityService", e);
147            }
148        }
149    
150        /**
151         * Create a Context object that also contains the globalContext.
152         *
153         * @return A Context object.
154         */
155        public Context getContext()
156        {
157            Context globalContext =
158                    pullModelActive ? pullService.getGlobalContext() : null;
159    
160            Context ctx = new VelocityContext(globalContext);
161            return ctx;
162        }
163    
164        /**
165         * This method returns a new, empty Context object.
166         *
167         * @return A Context Object.
168         */
169        public Context getNewContext()
170        {
171            Context ctx = new VelocityContext();
172    
173            // Attach an Event Cartridge to it, so we get exceptions
174            // while invoking methods from the Velocity Screens
175            EventCartridge ec = new EventCartridge();
176            ec.addEventHandler(this);
177            ec.attachToContext(ctx);
178            return ctx;
179        }
180    
181        /**
182         * MethodException Event Cartridge handler
183         * for Velocity.
184         *
185         * It logs an execption thrown by the velocity processing
186         * on error level into the log file
187         *
188         * @param clazz The class that threw the exception
189         * @param method The Method name that threw the exception
190         * @param e The exception that would've been thrown
191         * @return A valid value to be used as Return value
192         * @throws Exception We threw the exception further up
193         */
194        public Object methodException(Class clazz, String method, Exception e)
195                throws Exception
196        {
197            log.error("Class " + clazz.getName() + "." + method + " threw Exception", e);
198    
199            if (!catchErrors)
200            {
201                throw e;
202            }
203    
204            return "[Turbine caught an Error here. Look into the turbine.log for further information]";
205        }
206    
207        /**
208         * Create a Context from the RunData object.  Adds a pointer to
209         * the RunData object to the VelocityContext so that RunData
210         * is available in the templates.
211         * @deprecated. Use PipelineData version.
212         * @param data The Turbine RunData object.
213         * @return A clone of the WebContext needed by Velocity.
214         */
215        public Context getContext(RunData data)
216        {
217            // Attempt to get it from the data first.  If it doesn't
218            // exist, create it and then stuff it into the data.
219            Context context = (Context)
220                data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
221    
222            if (context == null)
223            {
224                context = getContext();
225                context.put(VelocityService.RUNDATA_KEY, data);
226    
227                if (pullModelActive)
228                {
229                    // Populate the toolbox with request scope, session scope
230                    // and persistent scope tools (global tools are already in
231                    // the toolBoxContent which has been wrapped to construct
232                    // this request-specific context).
233                    pullService.populateContext(context, data);
234                }
235    
236                data.getTemplateInfo().setTemplateContext(
237                    VelocityService.CONTEXT, context);
238            }
239            return context;
240        }
241    
242        /**
243         * Create a Context from the PipelineData object.  Adds a pointer to
244         * the RunData object to the VelocityContext so that RunData
245         * is available in the templates.
246         *
247         * @param data The Turbine RunData object.
248         * @return A clone of the WebContext needed by Velocity.
249         */
250        public Context getContext(PipelineData pipelineData)
251        {
252            //Map runDataMap = (Map)pipelineData.get(RunData.class);
253            RunData data = (RunData)pipelineData;
254            // Attempt to get it from the data first.  If it doesn't
255            // exist, create it and then stuff it into the data.
256            Context context = (Context)
257                data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
258    
259            if (context == null)
260            {
261                context = getContext();
262                context.put(VelocityService.RUNDATA_KEY, data);
263                // we will add both data and pipelineData to the context.
264                context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
265    
266                if (pullModelActive)
267                {
268                    // Populate the toolbox with request scope, session scope
269                    // and persistent scope tools (global tools are already in
270                    // the toolBoxContent which has been wrapped to construct
271                    // this request-specific context).
272                    pullService.populateContext(context, pipelineData);
273                }
274    
275                data.getTemplateInfo().setTemplateContext(
276                    VelocityService.CONTEXT, context);
277            }
278            return context;
279        }
280    
281        /**
282         * Process the request and fill in the template with the values
283         * you set in the Context.
284         *
285         * @param context  The populated context.
286         * @param filename The file name of the template.
287         * @return The process template as a String.
288         *
289         * @throws TurbineException Any exception trown while processing will be
290         *         wrapped into a TurbineException and rethrown.
291         */
292        public String handleRequest(Context context, String filename)
293            throws TurbineException
294        {
295            String results = null;
296            ByteArrayOutputStream bytes = null;
297            OutputStreamWriter writer = null;
298            String charset = getCharSet(context);
299    
300            try
301            {
302                bytes = new ByteArrayOutputStream();
303    
304                writer = new OutputStreamWriter(bytes, charset);
305    
306                executeRequest(context, filename, writer);
307                writer.flush();
308                results = bytes.toString(charset);
309            }
310            catch (Exception e)
311            {
312                renderingError(filename, e);
313            }
314            finally
315            {
316                try
317                {
318                    if (bytes != null)
319                    {
320                        bytes.close();
321                    }
322                }
323                catch (IOException ignored)
324                {
325                    // do nothing.
326                }
327            }
328            return results;
329        }
330    
331        /**
332         * Process the request and fill in the template with the values
333         * you set in the Context.
334         *
335         * @param context A Context.
336         * @param filename A String with the filename of the template.
337         * @param output A OutputStream where we will write the process template as
338         * a String.
339         *
340         * @throws TurbineException Any exception trown while processing will be
341         *         wrapped into a TurbineException and rethrown.
342         */
343        public void handleRequest(Context context, String filename,
344                                  OutputStream output)
345                throws TurbineException
346        {
347            String charset  = getCharSet(context);
348            OutputStreamWriter writer = null;
349    
350            try
351            {
352                writer = new OutputStreamWriter(output, charset);
353                executeRequest(context, filename, writer);
354            }
355            catch (Exception e)
356            {
357                renderingError(filename, e);
358            }
359            finally
360            {
361                try
362                {
363                    if (writer != null)
364                    {
365                        writer.flush();
366                    }
367                }
368                catch (Exception ignored)
369                {
370                    // do nothing.
371                }
372            }
373        }
374    
375    
376        /**
377         * Process the request and fill in the template with the values
378         * you set in the Context.
379         *
380         * @param context A Context.
381         * @param filename A String with the filename of the template.
382         * @param writer A Writer where we will write the process template as
383         * a String.
384         *
385         * @throws TurbineException Any exception trown while processing will be
386         *         wrapped into a TurbineException and rethrown.
387         */
388        public void handleRequest(Context context, String filename, Writer writer)
389                throws TurbineException
390        {
391            try
392            {
393                executeRequest(context, filename, writer);
394            }
395            catch (Exception e)
396            {
397                renderingError(filename, e);
398            }
399            finally
400            {
401                try
402                {
403                    if (writer != null)
404                    {
405                        writer.flush();
406                    }
407                }
408                catch (Exception ignored)
409                {
410                    // do nothing.
411                }
412            }
413        }
414    
415    
416        /**
417         * Process the request and fill in the template with the values
418         * you set in the Context. Apply the character and template
419         * encodings from RunData to the result.
420         *
421         * @param context A Context.
422         * @param filename A String with the filename of the template.
423         * @param writer A OutputStream where we will write the process template as
424         * a String.
425         *
426         * @throws Exception A problem occured.
427         */
428        private void executeRequest(Context context, String filename,
429                                    Writer writer)
430                throws Exception
431        {
432            String encoding = getEncoding(context);
433    
434            if (encoding == null)
435            {
436              encoding = DEFAULT_CHAR_SET;
437            }
438                    Velocity.mergeTemplate(filename, encoding, context, writer);
439        }
440    
441        /**
442         * Retrieve the required charset from the Turbine RunData in the context
443         *
444         * @param context A Context.
445         * @return The character set applied to the resulting String.
446         */
447        private String getCharSet(Context context)
448        {
449            String charset = null;
450    
451            Object data = context.get(VelocityService.RUNDATA_KEY);
452            if ((data != null) && (data instanceof RunData))
453            {
454                charset = ((RunData) data).getCharSet();
455            }
456    
457            return (StringUtils.isEmpty(charset)) ? DEFAULT_CHAR_SET : charset;
458        }
459    
460        /**
461         * Retrieve the required encoding from the Turbine RunData in the context
462         *
463         * @param context A Context.
464         * @return The encoding applied to the resulting String.
465         */
466        private String getEncoding(Context context)
467        {
468            String encoding = null;
469    
470            Object data = context.get(VelocityService.RUNDATA_KEY);
471            if ((data != null) && (data instanceof RunData))
472            {
473                encoding = ((RunData) data).getTemplateEncoding();
474            }
475    
476            return encoding;
477        }
478    
479        /**
480         * Macro to handle rendering errors.
481         *
482         * @param filename The file name of the unrenderable template.
483         * @param e        The error.
484         *
485         * @exception TurbineException Thrown every time.  Adds additional
486         *                             information to <code>e</code>.
487         */
488        private static final void renderingError(String filename, Exception e)
489                throws TurbineException
490        {
491            String err = "Error rendering Velocity template: " + filename;
492            log.error(err, e);
493            throw new TurbineException(err, e);
494        }
495    
496        /**
497         * Setup the velocity runtime by using a subset of the
498         * Turbine configuration which relates to velocity.
499         *
500         * @exception Exception An Error occured.
501         */
502        private synchronized void initVelocity()
503            throws Exception
504        {
505            // Get the configuration for this service.
506            Configuration conf = getConfiguration();
507    
508            catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
509    
510            conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
511                    Log4JLogChute.class.getName());
512            conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM
513                    + ".log4j.category", "velocity");
514    
515            Velocity.setExtendedProperties(createVelocityProperties(conf));
516            Velocity.init();
517        }
518    
519    
520        /**
521         * This method generates the Extended Properties object necessary
522         * for the initialization of Velocity. It also converts the various
523         * resource loader pathes into webapp relative pathes. It also
524         *
525         * @param conf The Velocity Service configuration
526         *
527         * @return An ExtendedProperties Object for Velocity
528         *
529         * @throws Exception If a problem occured while converting the properties.
530         */
531    
532        public ExtendedProperties createVelocityProperties(Configuration conf)
533                throws Exception
534        {
535            // This bugger is public, because we want to run some Unit tests
536            // on it.
537    
538            ExtendedProperties veloConfig = new ExtendedProperties();
539    
540            // Fix up all the template resource loader pathes to be
541            // webapp relative. Copy all other keys verbatim into the
542            // veloConfiguration.
543    
544            for (Iterator i = conf.getKeys(); i.hasNext();)
545            {
546                String key = (String) i.next();
547                if (!key.endsWith(RESOURCE_LOADER_PATH))
548                {
549                    Object value = conf.getProperty(key);
550                    if (value instanceof List) {
551                        for (Iterator itr = ((List)value).iterator(); itr.hasNext();)
552                        {
553                            veloConfig.addProperty(key, itr.next());
554                        }
555                    }
556                    else
557                    {
558                        veloConfig.addProperty(key, value);
559                    }
560                    continue; // for()
561                }
562    
563                List paths = conf.getList(key, null);
564                if (paths == null)
565                {
566                    // We don't copy this into VeloProperties, because
567                    // null value is unhealthy for the ExtendedProperties object...
568                    continue; // for()
569                }
570    
571                Velocity.clearProperty(key);
572    
573                // Translate the supplied pathes given here.
574                // the following three different kinds of
575                // pathes must be translated to be webapp-relative
576                //
577                // jar:file://path-component!/entry-component
578                // file://path-component
579                // path/component
580                for (Iterator j = paths.iterator(); j.hasNext();)
581                {
582                    String path = (String) j.next();
583    
584                    log.debug("Translating " + path);
585    
586                    if (path.startsWith(JAR_PREFIX))
587                    {
588                        // skip jar: -> 4 chars
589                        if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
590                        {
591                            // We must convert up to the jar path separator
592                            int jarSepIndex = path.indexOf("!/");
593    
594                            // jar:file:// -> skip 11 chars
595                            path = (jarSepIndex < 0)
596                                ? Turbine.getRealPath(path.substring(11))
597                            // Add the path after the jar path separator again to the new url.
598                                : (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
599    
600                            log.debug("Result (absolute jar path): " + path);
601                        }
602                    }
603                    else if(path.startsWith(ABSOLUTE_PREFIX))
604                    {
605                        // skip file:// -> 7 chars
606                        path = Turbine.getRealPath(path.substring(7));
607    
608                        log.debug("Result (absolute URL Path): " + path);
609                    }
610                    // Test if this might be some sort of URL that we haven't encountered yet.
611                    else if(path.indexOf("://") < 0)
612                    {
613                        path = Turbine.getRealPath(path);
614    
615                        log.debug("Result (normal fs reference): " + path);
616                    }
617    
618                    log.debug("Adding " + key + " -> " + path);
619                    // Re-Add this property to the configuration object
620                    veloConfig.addProperty(key, path);
621                }
622            }
623            return veloConfig;
624        }
625    
626        /**
627         * Find out if a given template exists. Velocity
628         * will do its own searching to determine whether
629         * a template exists or not.
630         *
631         * @param template String template to search for
632         * @return True if the template can be loaded by Velocity
633         */
634        @Override
635        public boolean templateExists(String template)
636        {
637            return Velocity.resourceExists(template);
638        }
639    
640        /**
641         * Performs post-request actions (releases context
642         * tools back to the object pool).
643         *
644         * @param context a Velocity Context
645         */
646        public void requestFinished(Context context)
647        {
648            if (pullModelActive)
649            {
650                pullService.releaseTools(context);
651            }
652        }
653    }