View Javadoc

1   package org.apache.fulcrum.template.velocity;
2   
3   
4   /*
5    * Licensed to the Apache Software Foundation (ASF) under one
6    * or more contributor license agreements.  See the NOTICE file
7    * distributed with this work for additional information
8    * regarding copyright ownership.  The ASF licenses this file
9    * to you under the Apache License, Version 2.0 (the
10   * "License"); you may not use this file except in compliance
11   * with the License.  You may obtain a copy of the License at
12   *
13   *   http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing,
16   * software distributed under the License is distributed on an
17   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18   * KIND, either express or implied.  See the License for the
19   * specific language governing permissions and limitations
20   * under the License.
21   */
22  
23  
24  import java.io.ByteArrayOutputStream;
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.OutputStream;
28  import java.io.OutputStreamWriter;
29  import java.io.Writer;
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Vector;
35  
36  import org.apache.avalon.framework.activity.Initializable;
37  import org.apache.avalon.framework.configuration.Configuration;
38  import org.apache.avalon.framework.configuration.ConfigurationException;
39  import org.apache.commons.configuration.ConfigurationConverter;
40  import org.apache.fulcrum.template.BaseTemplateEngineService;
41  import org.apache.fulcrum.template.TemplateContext;
42  import org.apache.fulcrum.template.TemplateException;
43  import org.apache.velocity.VelocityContext;
44  import org.apache.velocity.app.VelocityEngine;
45  import org.apache.velocity.app.event.EventCartridge;
46  import org.apache.velocity.app.event.MethodExceptionEventHandler;
47  import org.apache.velocity.app.event.NullSetEventHandler;
48  import org.apache.velocity.app.event.ReferenceInsertionEventHandler;
49  import org.apache.velocity.context.Context;
50  import org.apache.velocity.context.InternalEventContext;
51  import org.apache.velocity.exception.MethodInvocationException;
52  
53  /**
54   * This is a Service that can process Velocity templates from within a
55   * Turbine Screen.  Here's an example of how you might use it from a
56   * screen:<br>
57   *
58   * <code><pre>
59   * Context context = new VelocityContext();
60   * context.put("message", "Hello from Turbine!");
61   * String results = VelocityServiceFacade.handleRequest(context,"HelloWorld.vm");
62   * </pre></code>
63   *
64   * Character sets map codes to glyphs, while encodings map between
65   * chars/bytes and codes.
66   * <i>bytes -> [encoding] -> charset -> [rendering] -> glyphs</i>
67   *
68   * @author <a href="mailto:epugh@opensourceconnections.com">Eric Pugh</a>
69   * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
70   * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
71   * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
72   * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
73   * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
74   * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
75   * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
76   * @author <a href="mailto:james@jamestaylor.org">James Taylor</a>
77   * @version $Id: DefaultVelocityService.java 535465 2007-05-05 06:58:06Z tv $
78   */
79  public class DefaultVelocityService
80      extends BaseTemplateEngineService
81      implements VelocityService, Initializable
82  {
83      /**
84       * The generic resource loader path property in velocity.
85       */
86      private static final String RESOURCE_LOADER_PATH =
87          ".resource.loader.path";
88  
89      /**
90       * Default character set to use if not specified by caller.
91       */
92      private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
93  
94      /**
95       * The prefix used for URIs which are of type <code>jar</code>.
96       */
97      private static final String JAR_PREFIX = "jar:";
98  
99      /**
100      * The prefix used for URIs which are of type <code>absolute</code>.
101      */
102     private static final String ABSOLUTE_PREFIX = "file://";
103 
104     /**
105      * The EventCartridge that is used against all contexts
106      */
107     private EventCartridge eventCartridge;
108 
109     /**
110      * Whether or not to use the eventCartridge. Defaults to true.
111      * Can be used to turn off EC processing.
112      */
113     private boolean eventCartridgeEnabled = true;
114 
115 
116     // put conf into object to pass to the velocity engine
117     org.apache.commons.configuration.Configuration velocityConf;
118 
119     /**
120      * The VelocityEngine used by the service to merge templates
121      */
122     private VelocityEngine velocityEngine;
123 
124     /**
125      * @see org.apache.fulcrum.velocity.VelocityService
126      */
127     public String handleRequest(TemplateContext context, String template)
128         throws TemplateException
129     {
130         return handleRequest(new ContextAdapter(context), template);
131     }
132 
133     /**
134      * @see org.apache.fulcrum.velocity.VelocityService
135      */
136     public String handleRequest(Context context, String filename)
137         throws TemplateException
138     {
139         return handleRequest(context, filename, (String)null, null);
140     }
141 
142     /**
143      * @see org.apache.fulcrum.velocity.VelocityService
144      */
145     public String handleRequest(Context context, String filename,
146                                 String charset, String encoding)
147         throws TemplateException
148     {
149         String results = null;
150         ByteArrayOutputStream bytes = null;
151 
152         try
153         {
154             bytes = new ByteArrayOutputStream();
155             charset = decodeRequest(context, filename, bytes, charset,
156                                     encoding);
157             results = bytes.toString(charset);
158         }
159         catch (Exception e)
160         {
161             renderingError(filename, e);
162         }
163         finally
164         {
165             try
166             {
167                 if (bytes != null)
168                 {
169                     bytes.close();
170                 }
171             }
172             catch (IOException ignored)
173             {
174             }
175         }
176         return results;
177     }
178 
179     /**
180      * @see org.apache.fulcrum.template.TemplateEngineService
181      */
182     public void handleRequest(TemplateContext context, String template,
183                               OutputStream outputStream)
184         throws TemplateException
185     {
186         handleRequest(new ContextAdapter(context), template, outputStream);
187     }
188 
189     /**
190      * @see org.apache.fulcrum.velocity.VelocityService
191      */
192     public void handleRequest(Context context, String filename,
193                               OutputStream output)
194         throws TemplateException
195     {
196         handleRequest(context, filename, output, null, null);
197     }
198 
199     /**
200      * @see org.apache.fulcrum.velocity.VelocityService
201      */
202     public void handleRequest(Context context, String filename,
203                               OutputStream output, String charset,
204                               String encoding)
205         throws TemplateException
206     {
207         decodeRequest(context, filename, output, charset, encoding);
208     }
209 
210     /**
211      * @see BaseTemplateEngineService#handleRequest(TemplateContext, String, Writer)
212      */
213     public void handleRequest(TemplateContext context,
214                                        String template, Writer writer)
215         throws TemplateException
216     {
217         handleRequest(new ContextAdapter(context), template, writer);
218     }
219 
220     /**
221      * @see VelocityService#handleRequest(Context, String, Writer)
222      */
223     public void handleRequest(Context context, String filename,
224                               Writer writer)
225         throws TemplateException
226     {
227         handleRequest(context, filename, writer, null);
228     }
229 
230     /**
231      * @see VelocityService#handleRequest(Context, String, Writer, String)
232      */
233     public void handleRequest(Context context, String filename,
234                               Writer writer, String encoding)
235         throws TemplateException
236     {
237         try
238         {
239             // If the context is not already an instance of
240             // InternalEventContext, wrap it in a VeclocityContext so that
241             // event cartridges will work. Unfortunately there is no interface
242             // that extends both Context and InternalEventContext, so this
243             // is not as clear as it could be.
244 
245             Context eventContext;
246 
247             if ( context instanceof InternalEventContext )
248             {
249                 eventContext = context;
250             }
251             else
252             {
253                 eventContext = new VelocityContext( context );
254             }
255 
256             // Attach the EC to the context
257             EventCartridge ec = getEventCartridge();
258             if (ec != null && eventCartridgeEnabled)
259             {
260                 ec.attachToContext(eventContext);
261             }
262 
263             if (encoding != null)
264             {
265                 // Request scoped encoding first supported by Velocity 1.1.
266                 velocityEngine.mergeTemplate(filename, encoding,
267                                              eventContext, writer);
268             }
269             else
270             {
271                 velocityEngine.mergeTemplate(filename, eventContext, writer);
272             }
273         }
274         catch (Exception e)
275         {
276             renderingError(filename, e);
277         }
278     }
279 
280     /**
281      * By default, this is true if there is configured event cartridges.
282      * You can disable EC processing if you first disable it and then call
283      * handleRequest.
284      */
285     public void setEventCartridgeEnabled(boolean value)
286     {
287         this.eventCartridgeEnabled = value;
288     }
289 
290     /**
291      * @return EventCartridge the event cartridge
292      */
293     public EventCartridge getEventCartridge()
294     {
295         return eventCartridge;
296     }
297 
298     /**
299      * Processes the request and fill in the template with the values
300      * you set in the the supplied Context. Applies the specified
301      * character and template encodings.
302      *
303      * @param context A context to use when evaluating the specified
304      * template.
305      * @param filename The file name of the template.
306      * @param output The stream to which we will write the processed
307      * template as a String.
308      * @return The character set applied to the resulting text.
309      *
310      * @throws ServiceException Any exception trown while processing
311      * will be wrapped into a ServiceException and rethrown.
312      */
313     private String decodeRequest(Context context, String filename,
314                                  OutputStream output, String charset,
315                                  String encoding)
316         throws TemplateException
317     {
318         // TODO: Push this method of getting character set & encoding
319         // from RunData back into Turbine.
320         // charset  = ((RunData) data).getCharSet();
321         // encoding = ((RunData) data).getTemplateEncoding();
322 
323         if (charset == null)
324         {
325             charset = DEFAULT_CHAR_SET;
326         }
327 
328         OutputStreamWriter writer = null;
329         try
330         {
331             try
332             {
333                 writer = new OutputStreamWriter(output, charset);
334             }
335             catch (Exception e)
336             {
337                 renderingError(filename, e);
338             }
339             handleRequest(context, filename, writer, encoding);
340         }
341         finally
342         {
343             try
344             {
345                 if (writer != null)
346                 {
347                     writer.flush();
348                 }
349             }
350             catch (Exception ignored)
351             {
352             }
353         }
354         return charset;
355     }
356 
357     /**
358      * Macro to handle rendering errors.
359      *
360      * @param filename The file name of the unrenderable template.
361      * @param e        The error.
362      *
363      * @exception ServiceException Thrown every time.  Adds additional
364      *                             information to <code>e</code>.
365      */
366     private final void renderingError(String filename, Throwable e)
367         throws TemplateException
368     {
369         String err = "Error rendering Velocity template: " + filename;
370         getLogger().error(err + ": " + e.getMessage());
371         // if the Exception is a MethodInvocationException, the underlying
372         // Exception is likely to be more informative, so rewrap that one.
373         if (e instanceof MethodInvocationException)
374         {
375             e = ((MethodInvocationException)e).getWrappedThrowable();
376         }
377 
378         throw new TemplateException(err, e);
379     }
380 
381     /**
382      * Find out if a given template exists. Velocity
383      * will do its own searching to determine whether
384      * a template exists or not.
385      *
386      * @param String template to search for
387      * @return boolean
388      */
389     public boolean templateExists(String template)
390     {
391         return velocityEngine.templateExists(template);
392     }
393 
394     // ---------------- Avalon Lifecycle Methods ---------------------
395 
396     /**
397      * Avalon component lifecycle method
398      */
399     public void configure(Configuration conf)
400         throws ConfigurationException
401     {
402         // put conf into object to pass to the velocity engine
403         velocityConf =
404             new org.apache.commons.configuration.BaseConfiguration();
405         List ecconfig = null;
406         String logPath = null;
407         List templatePathKeys = new ArrayList();
408         List templatePaths = new ArrayList();
409 
410         if (1==2)
411         {
412             /* ### FIXME: Why is this both setup in a block which
413                ### won't be compiled, and commented out?
414             org.apache.commons.configuration.Configuration oldConf =
415                 getConfiguration();
416 
417             ecconfig = oldConf
418                 .getVector("eventCartridge.classes", new Vector(0));
419             if (ecconfig.isEmpty())
420             {
421                 getLogger().info("No Velocity EventCartridges configured.");
422             }
423 
424             // Now we have to perform a couple of path translations
425             // for our log file and template paths.
426             logPath =
427                 oldConf.getString(VelocityEngine.RUNTIME_LOG, null);
428             if (logPath == null || logPath.length() == 0)
429             {
430                 String msg = VelocityService.SERVICE_NAME+" runtime log file "+
431                     "is misconfigured: '" + logPath + "' is not a valid log file";
432                 throw new Error(msg);
433             }
434 
435             // Get all the template paths where the velocity
436             // runtime should search for templates and
437             // collect them into a separate vector
438             // to avoid concurrent modification exceptions.
439             for (Iterator i = oldConf.getKeys(); i.hasNext();)
440             {
441                 String key = (String) i.next();
442                 if (key.endsWith(RESOURCE_LOADER_PATH))
443                 {
444                     templatePathKeys.add(key);
445                     templatePaths.add(oldConf.getVector(key));
446                 }
447             }
448             */
449         }
450         else
451         {
452             // trick compiler
453             if (true)
454             {
455                 throw new ConfigurationException(
456                     "Use of avalon-style configuration not completed yet");
457             }
458 
459             final Configuration eventCartridgeConfs =
460                 conf.getChild("event-cartriges", false);
461             if (eventCartridgeConfs == null)
462             {
463                 ecconfig = Collections.EMPTY_LIST;
464             }
465             else
466             {
467                 Configuration[] classNameConfs =
468                     eventCartridgeConfs.getChildren("classname");
469                 if (classNameConfs == null)
470                 {
471                     ecconfig = Collections.EMPTY_LIST;
472                 }
473                 else
474                 {
475                     ecconfig = new ArrayList(classNameConfs.length);
476                     for (int i=0; i < classNameConfs.length; i++)
477                     {
478                         ecconfig.add(classNameConfs[i].getValue());
479                     }
480                 }
481             }
482 
483             /*
484              *            final Configuration pathConfs =
485              *                conf.getChild("event-cartriges", false);
486              *            if (pathConfs != null)
487              *            {
488              *                Configuration[] nameVal = ecConfs.getChildren();
489              *                for (int i=0; i < nameVal.length; i++)
490              *                {
491              *                    String key = nameVal[i].getName();
492              *                    String val = nameVal[i].getValue();
493              *                }
494              *            }
495              */
496         }
497 
498         initEventCartridges(ecconfig);
499 
500         // check if path to logfile needs translation to webapp root
501         if ( !(new File(logPath).isAbsolute()) )
502         {
503             logPath = getRealPath(logPath);
504         }
505         velocityConf.setProperty(VelocityEngine.RUNTIME_LOG, logPath);
506 
507         configureTemplatePaths(templatePathKeys, templatePaths);
508 
509         // Register with the template service.
510         registerConfiguration(conf, "vm");
511     }
512 
513     /**
514      * This method is responsible for initializing the various Velocity
515      * EventCartridges. You just add a configuration like this:
516      * <code>
517      * services.VelocityService.eventCartridge.classes = org.tigris.scarab.util.ReferenceInsertionFilter
518      * </code>
519      * and list out (comma separated) the list of EC's that you want to
520      * initialize.
521      */
522     private void initEventCartridges(List ecconfig)
523         throws ConfigurationException
524     {
525         eventCartridge = new EventCartridge();
526         Object obj = null;
527         String className = null;
528         for (Iterator i = ecconfig.iterator() ; i.hasNext() ;)
529         {
530             className = (String)i.next();
531             try
532             {
533                 boolean result = false;
534 
535                 obj = Class.forName(className).newInstance();
536 
537                 if (obj instanceof ReferenceInsertionEventHandler)
538                 {
539                     result = getEventCartridge()
540                         .addEventHandler((ReferenceInsertionEventHandler)obj);
541                 }
542                 else if (obj instanceof NullSetEventHandler)
543                 {
544                     result = getEventCartridge()
545                         .addEventHandler((NullSetEventHandler)obj);
546                 }
547                 else if (obj instanceof MethodExceptionEventHandler)
548                 {
549                     result = getEventCartridge()
550                         .addEventHandler((MethodExceptionEventHandler)obj);
551                 }
552                 getLogger().info("Added EventCartridge: " +
553                     obj.getClass().getName() + " : " + result);
554             }
555             catch (Exception h)
556             {
557                 throw new ConfigurationException(
558                     "Could not configure EventCartridge: " +
559                     className, h);
560             }
561         }
562     }
563 
564     /**
565      * Setup the velocity runtime by using a subset of the
566      * Turbine configuration which relates to velocity.
567      *
568      * @exception ConfigurationException For any errors during initialization.
569      */
570     private void configureTemplatePaths(
571         List templatePathKeys, List templatePaths)
572     {
573         // Loop through all template paths, clear the corresponding
574         // velocity properties and translate them all to the webapp space.
575         for (int i=0; i<templatePathKeys.size(); i++)
576         {
577             String key = (String) templatePathKeys.get(i);
578             Vector paths = (Vector) templatePaths.get(i);
579             if (paths != null)
580             {
581                 String entry;
582                 for (Iterator j = paths.iterator(); j.hasNext();)
583                 {
584                     String path = (String) j.next();
585                     if (path.startsWith(JAR_PREFIX + "file"))
586                     {
587                         // A local jar resource URL path is a bit more
588                         // complicated, but we can translate it as well.
589                         int ind = path.indexOf("!/");
590                         if (ind >= 0)
591                         {
592                             entry = path.substring(ind);
593                             path = path.substring(9,ind);
594                         }
595                         else
596                         {
597                             entry = "!/";
598                             path = path.substring(9);
599                         }
600                         path = JAR_PREFIX + "file:" + getRealPath(path) +
601                             entry;
602                     }
603                     else if (path.startsWith(ABSOLUTE_PREFIX))
604                     {
605                         path = path.substring (ABSOLUTE_PREFIX.length(),
606                                                path.length());
607                     }
608                     else if (!path.startsWith(JAR_PREFIX))
609                     {
610                         // But we don't translate remote jar URLs.
611                         path = getRealPath(path);
612                     }
613                     // Put the translated paths back to the configuration.
614                     velocityConf.addProperty(key,path);
615                 }
616             }
617         }
618     }
619 
620     // ---------------- Avalon Lifecycle Methods ---------------------
621 
622     /**
623      * Avalon component lifecycle method
624      */
625     public void initialize()
626         throws Exception
627     {
628         try
629         {
630             velocityEngine = new VelocityEngine();
631 
632             // clear the property to prepare for new value,
633             //is this needed?
634             Iterator i = velocityConf.getKeys();
635             while (i.hasNext())
636             {
637                 velocityEngine.clearProperty((String)i.next());
638             }
639 
640             velocityEngine.setExtendedProperties(ConfigurationConverter
641                     .getExtendedProperties(velocityConf));
642             velocityEngine.init();
643             velocityConf = null;
644 
645         }
646         catch (Exception e)
647         {
648             e.printStackTrace();
649             throw new Exception(
650                 "Failed to initialize DefaultVelocityService", e);
651         }
652     }
653 
654 }