View Javadoc

1   package org.andromda.templateengines.velocity;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.io.StringWriter;
7   import java.io.Writer;
8   import java.net.URL;
9   import java.util.ArrayList;
10  import java.util.Collection;
11  import java.util.HashMap;
12  import java.util.Iterator;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Properties;
16  
17  import org.andromda.core.common.AndroMDALogger;
18  import org.andromda.core.common.Constants;
19  import org.andromda.core.common.ExceptionUtils;
20  import org.andromda.core.common.Merger;
21  import org.andromda.core.common.ResourceUtils;
22  import org.andromda.core.common.ResourceWriter;
23  import org.andromda.core.templateengine.TemplateEngine;
24  import org.andromda.core.templateengine.TemplateEngineException;
25  import org.apache.commons.collections.ExtendedProperties;
26  import org.apache.commons.lang.StringUtils;
27  import org.apache.log4j.FileAppender;
28  import org.apache.log4j.Logger;
29  import org.apache.log4j.PatternLayout;
30  import org.apache.velocity.Template;
31  import org.apache.velocity.VelocityContext;
32  import org.apache.velocity.app.VelocityEngine;
33  import org.apache.velocity.runtime.RuntimeServices;
34  import org.apache.velocity.runtime.log.LogSystem;
35  import org.apache.velocity.runtime.resource.loader.FileResourceLoader;
36  
37  
38  /***
39   * The TemplateEngine implementation for VelocityTemplateEngine template processor.
40   *
41   * @author <a href="http://www.mbohlen.de">Matthias Bohlen </a>
42   * @author Chad Brandon
43   * @see http://jakarta.apache.org/velocity/
44   */
45  public class VelocityTemplateEngine
46      implements TemplateEngine
47  {
48      protected static Logger logger = null;
49  
50      /***
51       * The directory we look in to find velocity properties.
52       */
53      private static final String PROPERTIES_DIR = "META-INF/";
54  
55      /***
56       * The suffix for the the velocity properties.
57       */
58      private static final String PROPERTIES_SUFFIX = "-velocity.properties";
59  
60      /***
61       * The location to which temporary templates are written
62       */
63      private static final String TEMPORARY_TEMPLATE_LOCATION = Constants.TEMPORARY_DIRECTORY + "velocity/merged";
64  
65      /***
66       * The location of external templates
67       */
68      private String mergeLocation;
69  
70      /***
71       * Stores additional properties specified within the plugin within the file META-INF/'plugin
72       * name'-velocity.properties
73       */
74      private Properties properties = null;
75  
76      /***
77       * The current namespace this template engine is running within.
78       */
79      private String namespace;
80  
81      /***
82       * the VelocityEngine instance to use
83       */
84      private VelocityEngine velocityEngine;
85      private VelocityContext velocityContext;
86      private final List macroLibraries = new ArrayList();
87  
88      /***
89       * Stores a collection of templates that have already been
90       * discovered by the velocity engine
91       */
92      private final Map discoveredTemplates = new HashMap();
93  
94      /***
95       * Stores the merged template files that are deleted at shutdown.
96       */
97      private final Collection mergedTemplateFiles = new ArrayList();
98  
99      /***
100      * @see org.andromda.core.templateengine.TemplateEngine#init(java.lang.String)
101      */
102     public void initialize(final String namespace)
103         throws Exception
104     {
105         this.namespace = namespace;
106         this.initLogger(namespace);
107 
108         ExtendedProperties engineProperties = new ExtendedProperties();
109 
110         // Tell VelocityTemplateEngine it should also use the
111         // classpath when searching for templates
112         // IMPORTANT: file,andromda.plugins the ordering of these
113         // two things matters, the ordering allows files to override
114         // the resources found on the classpath.
115         engineProperties.setProperty(VelocityEngine.RESOURCE_LOADER, "file,classpath");
116 
117         engineProperties.setProperty(
118             "file." + VelocityEngine.RESOURCE_LOADER + ".class",
119             FileResourceLoader.class.getName());
120 
121         engineProperties.setProperty(
122             "classpath." + VelocityEngine.RESOURCE_LOADER + ".class",
123             ClasspathResourceLoader.class.getName());
124 
125         // Tell VelocityTemplateEngine not to use its own logger
126         // the logger but to use of this plugin.
127         engineProperties.setProperty(
128             VelocityEngine.RUNTIME_LOG_LOGSYSTEM,
129             new VelocityLoggingReceiver());
130 
131         // Let this template engine know about the macro libraries.
132         for (final Iterator iterator = getMacroLibraries().iterator(); iterator.hasNext();)
133         {
134             engineProperties.addProperty(
135                 VelocityEngine.VM_LIBRARY,
136                 iterator.next());
137         }
138 
139         this.velocityEngine = new VelocityEngine();
140         this.velocityEngine.setExtendedProperties(engineProperties);
141 
142         if (this.mergeLocation != null)
143         {
144             // set the file resource path (to the merge location)
145             velocityEngine.addProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, this.mergeLocation);
146         }
147 
148         // if the namespace requires a merge add the temporary template
149         // location to which merged templates are written
150         if (Merger.instance().requiresMerge(this.namespace))
151         {
152             velocityEngine.addProperty(
153                 VelocityEngine.FILE_RESOURCE_LOADER_PATH,
154                 this.getMergedTemplatesLocation());
155         }
156 
157         this.addProperties(namespace);
158         this.velocityEngine.init();
159     }
160 
161     /***
162      * Adds any properties found within META-INF/'plugin name'-velocity.properties
163      */
164     private void addProperties(String pluginName)
165         throws IOException
166     {
167         // reset any properties from previous processing
168         this.properties = null;
169 
170         // see if the velocity properties exist for the current
171         // plugin
172         URL propertiesUri =
173             ResourceUtils.getResource(PROPERTIES_DIR + StringUtils.trimToEmpty(pluginName) + PROPERTIES_SUFFIX);
174 
175         if (propertiesUri != null)
176         {
177             if (logger.isDebugEnabled())
178             {
179                 logger.debug("loading properties from --> '" + propertiesUri + "'");
180             }
181 
182             this.properties = new Properties();
183             this.properties.load(propertiesUri.openStream());
184 
185             for (final Iterator iterator = this.properties.keySet().iterator(); iterator.hasNext();)
186             {
187                 final String property = (String)iterator.next();
188                 final String value = this.properties.getProperty(property);
189                 if (logger.isDebugEnabled())
190                 {
191                     logger.debug("setting property '" + property + "' with --> '" + value + "'");
192                 }
193                 this.velocityEngine.setProperty(property, value);
194             }
195         }
196     }
197 
198     /***
199      * @see org.andromda.core.templateengine.TemplateEngine#processTemplate(java.lang.String, java.util.Map,
200      *      java.io.StringWriter)
201      */
202     public void processTemplate(
203         final String templateFile,
204         final Map templateObjects,
205         final Writer output)
206         throws Exception
207     {
208         final String methodName = "VelocityTemplateEngine.processTemplate";
209 
210         if (logger.isDebugEnabled())
211         {
212             logger.debug(
213                 "performing " + methodName + " with templateFile '" + templateFile + "' and templateObjects '" +
214                 templateObjects + "'");
215         }
216         ExceptionUtils.checkEmpty("templateFile", templateFile);
217         ExceptionUtils.checkNull("output", output);
218         this.velocityContext = new VelocityContext();
219         this.loadVelocityContext(templateObjects);
220 
221         Template template = (Template)this.discoveredTemplates.get(templateFile);
222         if (template == null)
223         {
224             template = this.velocityEngine.getTemplate(templateFile);
225 
226             // We check to see if the namespace requires a merge, and if so 
227             final Merger merger = Merger.instance();
228             if (merger.requiresMerge(this.namespace))
229             {
230                 final String mergedTemplateLocation = this.getMergedTemplateLocation(templateFile);
231                 final InputStream resource = template.getResourceLoader().getResourceStream(templateFile);
232                 ResourceWriter.instance().writeStringToFile(
233                     merger.getMergedString(resource, this.namespace),
234                     mergedTemplateLocation);
235                 template = this.velocityEngine.getTemplate(templateFile);
236                 this.mergedTemplateFiles.add(new File(mergedTemplateLocation));
237             }
238             this.discoveredTemplates.put(templateFile, template);
239         }
240         template.merge(velocityContext, output);
241     }
242     
243     /***
244      * Loads the internal {@link #velocityContext} from the 
245      * given Map of template objects.
246      * 
247      * @param a Map containing objects to add to the template context.
248      */
249     private final void loadVelocityContext(final Map templateObjects)
250     {
251         if (templateObjects != null && !templateObjects.isEmpty())
252         {    
253             // copy the templateObjects to the velocityContext
254             if (templateObjects != null)
255             {
256                 for (final Iterator namesIterator = templateObjects.keySet().iterator(); namesIterator.hasNext();)
257                 {
258                     final String name = (String)namesIterator.next();
259                     final Object value = templateObjects.get(name);
260                     this.velocityContext.put(name, value);
261                 }
262             }
263         }
264     }
265 
266     /***
267      * Gets location to which the given <code>templateName</code>
268      * has its merged output written.
269      * @param templatePath the relative path to the template.
270      * @return the complete merged template location.
271      */
272     private String getMergedTemplateLocation(String templatePath)
273     {
274         return this.getMergedTemplatesLocation() + "/" + templatePath;
275     }
276 
277     /***
278      * Gets the location to which merge templates are written.  These
279      * must be written in order to replace the unmerged ones when Velocity
280      * performs its template search.
281      *
282      * @return the merged templates location.
283      */
284     private String getMergedTemplatesLocation()
285     {
286         return TEMPORARY_TEMPLATE_LOCATION + "/" + this.namespace;
287     }
288 
289     /***
290      * The log tag used for evaluation (this can be any abitrary name).
291      */
292     private static final String LOG_TAG = "logtag";
293     
294     /***
295      * @see org.andromda.core.templateengine.TemplateEngine#getEvaluatedExpression(java.lang.String, java.util.Map)
296      */
297     public String getEvaluatedExpression(final String expression, final Map templateObjects)
298     {
299         String evaluatedExpression = null;
300         if (StringUtils.isNotEmpty(expression) && templateObjects != null && !templateObjects.isEmpty())
301         {
302             if (this.velocityContext == null)
303             {
304                 this.velocityContext = new VelocityContext();
305                 this.loadVelocityContext(templateObjects);
306             }
307             try
308             {
309                 final StringWriter writer = new StringWriter();
310                 this.velocityEngine.evaluate(this.velocityContext, writer, LOG_TAG, expression);
311                 evaluatedExpression = writer.toString();
312             }
313             catch (final Throwable throwable)
314             {
315                 throw new TemplateEngineException(throwable);
316             }
317         }
318         return evaluatedExpression;
319     }
320 
321     /***
322      * @see org.andromda.core.templateengine.TemplateEngine#getMacroLibraries()
323      */
324     public List getMacroLibraries()
325     {
326         return this.macroLibraries;
327     }
328 
329     /***
330      * @see org.andromda.core.templateengine.TemplateEngine#addMacroLibrary(java.lang.String)
331      */
332     public void addMacroLibrary(String libraryName)
333     {
334         this.macroLibraries.add(libraryName);
335     }
336 
337     /***
338      * @see org.andromda.core.templateengine.TemplateEngine#setMergeLocation(java.lang.String)
339      */
340     public void setMergeLocation(String mergeLocation)
341     {
342         this.mergeLocation = mergeLocation;
343     }
344 
345     /***
346      * @see org.andromda.core.templateengine.TemplateEngine#shutdown()
347      */
348     public void shutdown()
349     {
350         this.deleteMergedTemplatesLocation();
351         this.discoveredTemplates.clear();
352         this.velocityEngine = null;
353     }
354 
355     /***
356      * Deletes the merged templates location (these
357      * are the templates that were created just for merging
358      * purposes and so therefore are no longer needed after
359      * the engine is shutdown).
360      */
361     private final void deleteMergedTemplatesLocation()
362     {
363         File directory = new File(TEMPORARY_TEMPLATE_LOCATION);
364         if (directory.getParentFile().isDirectory())
365         {
366             directory = directory.getParentFile();
367             ResourceUtils.deleteDirectory(directory);
368             directory.delete();
369         }
370     }
371 
372     /***
373      * Opens a log file for this namespace.
374      *
375      * @throws IOException if the file cannot be opened
376      */
377     private final void initLogger(final String pluginName)
378         throws IOException
379     {
380         logger = AndroMDALogger.getNamespaceLogger(pluginName);
381         logger.setAdditivity(false);
382 
383         final FileAppender appender =
384             new FileAppender(
385                 new PatternLayout("%-5p %d - %m%n"),
386                 AndroMDALogger.getNamespaceLogFileName(pluginName),
387                 true);
388         logger.addAppender(appender);
389     }
390 
391     /***
392      * <p/>
393      * This class receives log messages from VelocityTemplateEngine and forwards them to the concrete logger that is
394      * configured for this cartridge. </p>
395      * <p/>
396      * This avoids creation of one large VelocityTemplateEngine log file where errors are difficult to find and track.
397      * </p>
398      * <p/>
399      * Error messages can now be traced to plugin activities. </p>
400      */
401     private static class VelocityLoggingReceiver
402         implements LogSystem
403     {
404         /***
405          * @see org.apache.velocity.runtime.log.LogSystem#init(org.apache.velocity.runtime.RuntimeServices)
406          */
407         public void init(RuntimeServices services)
408             throws Exception
409         {
410         }
411 
412         /***
413          * @see org.apache.velocity.runtime.log.LogSystem#logVelocityMessage(int, java.lang.String)
414          */
415         public void logVelocityMessage(
416             int level,
417             String message)
418         {
419             switch (level)
420             {
421             case LogSystem.WARN_ID:
422                 logger.info(message);
423                 break;
424             case LogSystem.INFO_ID:
425                 logger.info(message);
426                 break;
427             case LogSystem.DEBUG_ID:
428                 logger.debug(message);
429                 break;
430             case LogSystem.ERROR_ID:
431                 logger.info(message);
432                 break;
433             default:
434                 logger.debug(message);
435                 break;
436             }
437         }
438     }
439 }