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.List;
13  import java.util.Map;
14  import java.util.Properties;
15  import org.andromda.core.common.AndroMDALogger;
16  import org.andromda.core.common.Constants;
17  import org.andromda.core.common.ExceptionUtils;
18  import org.andromda.core.common.Merger;
19  import org.andromda.core.common.ResourceUtils;
20  import org.andromda.core.common.ResourceWriter;
21  import org.andromda.core.templateengine.TemplateEngine;
22  import org.andromda.core.templateengine.TemplateEngineException;
23  import org.apache.commons.collections.ExtendedProperties;
24  import org.apache.commons.io.FileUtils;
25  import org.apache.commons.lang.StringUtils;
26  import org.apache.log4j.FileAppender;
27  import org.apache.log4j.Logger;
28  import org.apache.log4j.PatternLayout;
29  import org.apache.velocity.Template;
30  import org.apache.velocity.VelocityContext;
31  import org.apache.velocity.app.VelocityEngine;
32  import org.apache.velocity.runtime.RuntimeConstants;
33  import org.apache.velocity.runtime.log.Log4JLogChute;
34  import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
35  import org.apache.velocity.runtime.resource.loader.FileResourceLoader;
36  import org.apache.velocity.tools.generic.EscapeTool;
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      /**
49       * Log4J logger
50       */
51      protected static Logger logger = null;
52  
53      /**
54       * Log4J appender
55       */
56      protected FileAppender appender = null;
57  
58      /**
59       * The directory we look in to find velocity properties.
60       */
61      private static final String PROPERTIES_DIR = "META-INF/";
62  
63      /**
64       * The suffix for the the velocity properties.
65       */
66      private static final String PROPERTIES_SUFFIX = "-velocity.properties";
67  
68      /**
69       * The location to which temporary templates are written
70       */
71      private static final String TEMPORARY_TEMPLATE_LOCATION = Constants.TEMPORARY_DIRECTORY + "velocity/merged";
72  
73      /**
74       * The location of external templates
75       */
76      private String mergeLocation;
77  
78      /**
79       * The current namespace this template engine is running within.
80       */
81      private String namespace;
82  
83      /**
84       * The VelocityEngine instance to use
85       */
86      private VelocityEngine velocityEngine;
87      /**
88       * The VelocityContext instance to use
89       */
90      private VelocityContext velocityContext;
91      /**
92       * The Macro Libraries
93       */
94      private final List<String> macroLibraries = new ArrayList<String>();
95  
96      /**
97       * Stores a collection of templates that have already been
98       * discovered by the velocity engine
99       */
100     private final Map<String, Template> discoveredTemplates = new HashMap<String, Template>();
101 
102     /**
103      * Stores the merged template files that are deleted at shutdown.
104      */
105     private final Collection<File> mergedTemplateFiles = new ArrayList<File>();
106 
107     /**
108      * Initialized the engine
109      * @param namespace
110      * @throws Exception
111      * @see org.andromda.core.templateengine.TemplateEngine#initialize(String)
112      */
113     public void initialize(final String namespace)
114         throws Exception
115     {
116         this.namespace = namespace;
117         this.initLogger(namespace);
118 
119         ExtendedProperties engineProperties = new ExtendedProperties();
120 
121         // Tell VelocityTemplateEngine it should also use the
122         // classpath when searching for templates
123         // IMPORTANT: file,andromda.plugins the ordering of these
124         // two things matters, the ordering allows files to override
125         // the resources found on the classpath.
126         engineProperties.setProperty(VelocityEngine.RESOURCE_LOADER, "file,classpath");
127 
128         engineProperties.setProperty(
129             "file." + VelocityEngine.RESOURCE_LOADER + ".class",
130             FileResourceLoader.class.getName());
131 
132         engineProperties.setProperty(
133             "classpath." + VelocityEngine.RESOURCE_LOADER + ".class",
134             ClasspathResourceLoader.class.getName());
135 
136         // Configure Velocity logger
137         engineProperties.setProperty( RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
138               "org.apache.velocity.runtime.log.Log4JLogChute" );
139         engineProperties.setProperty(Log4JLogChute.RUNTIME_LOG_LOG4J_LOGGER, logger.getName());
140 
141         // Let this template engine know about the macro libraries.
142         for (String macroLibrary : getMacroLibraries())
143         {
144             engineProperties.addProperty(
145                     VelocityEngine.VM_LIBRARY,
146                     macroLibrary);
147         }
148 
149         this.velocityEngine = new VelocityEngine();
150         this.velocityEngine.setExtendedProperties(engineProperties);
151 
152         if (this.mergeLocation != null)
153         {
154             // set the file resource path (to the merge location)
155             velocityEngine.addProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, this.mergeLocation);
156         }
157 
158         // if the namespace requires a merge add the temporary template
159         // location to which merged templates are written
160         if (Merger.instance().requiresMerge(this.namespace))
161         {
162             velocityEngine.addProperty(
163                 VelocityEngine.FILE_RESOURCE_LOADER_PATH,
164                 this.getMergedTemplatesLocation());
165         }
166 
167         this.addProperties(namespace);
168         this.velocityEngine.init();
169     }
170 
171     /**
172      * Adds any properties found within META-INF/'plugin name'-velocity.properties
173      * @param pluginName name of the plugin
174      * @throws java.io.IOException if resource could not be found
175      */
176     private void addProperties(String pluginName)
177         throws IOException
178     {
179         // see if the velocity properties exist for the current plugin
180         URL propertiesUri =
181             ResourceUtils.getResource(PROPERTIES_DIR + StringUtils.trimToEmpty(pluginName) + PROPERTIES_SUFFIX);
182 
183         if (propertiesUri != null)
184         {
185             if (logger.isDebugEnabled())
186             {
187                 logger.debug("loading properties from --> '" + propertiesUri + '\'');
188             }
189 
190             Properties properties = new Properties();
191             properties.load(propertiesUri.openStream());
192 
193             for (Map.Entry entry : properties.entrySet())
194             {
195                 final String property = (String) entry.getKey();
196                 final String value = (String)entry.getValue();
197                 if (logger.isDebugEnabled())
198                 {
199                     logger.debug("setting property '" + property + "' with --> '" + value + '\'');
200                 }
201                 this.velocityEngine.setProperty(property, value);
202             }
203         }
204     }
205 
206     /**
207      * @see org.andromda.core.templateengine.TemplateEngine#processTemplate(String, java.util.Map,
208      *      java.io.Writer)
209      */
210     public void processTemplate(
211         final String templateFile,
212         final Map<String, Object> templateObjects,
213         final Writer output)
214         throws Exception
215     {
216         final String methodName = "VelocityTemplateEngine.processTemplate";
217 
218         if (logger.isDebugEnabled())
219         {
220             logger.debug(
221                 "performing " + methodName + " with templateFile '" + templateFile + "' and templateObjects '" +
222                 templateObjects + '\'');
223         }
224         ExceptionUtils.checkEmpty("templateFile", templateFile);
225         ExceptionUtils.checkNull("output", output);
226         this.velocityContext = new VelocityContext();
227         this.loadVelocityContext(templateObjects);
228 
229         Template template = this.discoveredTemplates.get(templateFile);
230         if (template == null)
231         {
232             template = this.velocityEngine.getTemplate(templateFile);
233 
234             // We check to see if the namespace requires a merge, and if so
235             final Merger merger = Merger.instance();
236             if (merger.requiresMerge(this.namespace))
237             {
238                 final String mergedTemplateLocation = this.getMergedTemplateLocation(templateFile);
239                 final InputStream resource = template.getResourceLoader().getResourceStream(templateFile);
240                 ResourceWriter.instance().writeStringToFile(
241                     merger.getMergedString(resource, this.namespace),
242                     mergedTemplateLocation);
243                 template = this.velocityEngine.getTemplate(templateFile);
244                 this.mergedTemplateFiles.add(new File(mergedTemplateLocation));
245                 resource.close();
246             }
247             this.discoveredTemplates.put(templateFile, template);
248         }
249         template.merge(velocityContext, output);
250     }
251 
252     /**
253      * Loads the internal {@link #velocityContext} from the
254      * given Map of template objects.
255      *
256      * @param templateObjects Map containing objects to add to the template context.
257      */
258     private void loadVelocityContext(final Map<String, Object> templateObjects)
259     {
260         if (templateObjects != null)
261         {
262             // copy the templateObjects to the velocityContext
263             for (Map.Entry<String, Object> entry : templateObjects.entrySet())
264             {
265                 this.velocityContext.put(entry.getKey(), entry.getValue());
266             }
267         }
268         // add velocity tools (Escape tool)
269         this.velocityContext.put("esc", new EscapeTool());
270     }
271 
272     /**
273      * Gets location to which the given <code>templateName</code>
274      * has its merged output written.
275      * @param templatePath the relative path to the template.
276      * @return the complete merged template location.
277      */
278     private String getMergedTemplateLocation(String templatePath)
279     {
280         return this.getMergedTemplatesLocation() + '/' + templatePath;
281     }
282 
283     /**
284      * Gets the location to which merge templates are written.  These
285      * must be written in order to replace the unmerged ones when Velocity
286      * performs its template search.
287      *
288      * @return the merged templates location.
289      */
290     private String getMergedTemplatesLocation()
291     {
292         return TEMPORARY_TEMPLATE_LOCATION + '/' + this.namespace;
293     }
294 
295     /**
296      * The log tag used for evaluation (this can be any abitrary name).
297      */
298     private static final String LOG_TAG = "logtag";
299 
300     /**
301      * @see org.andromda.core.templateengine.TemplateEngine#getEvaluatedExpression(String, java.util.Map)
302      */
303     public String getEvaluatedExpression(final String expression, final Map<String, Object> templateObjects)
304     {
305         String evaluatedExpression = null;
306         if (StringUtils.isNotBlank(expression))
307         {
308             // reuse last created context, need it for processing $generateFilename
309             if (this.velocityContext == null)
310             {
311                 this.velocityContext = new VelocityContext();
312                 this.loadVelocityContext(templateObjects);
313             }
314 
315             try
316             {
317                 final StringWriter writer = new StringWriter();
318                 this.velocityEngine.evaluate(this.velocityContext, writer, LOG_TAG, expression);
319                 evaluatedExpression = writer.toString();
320             }
321             catch (final Throwable throwable)
322             {
323                 throw new TemplateEngineException(throwable);
324             }
325         }
326         return evaluatedExpression;
327     }
328 
329     /**
330      * @see org.andromda.core.templateengine.TemplateEngine#getMacroLibraries()
331      */
332     public List<String> getMacroLibraries()
333     {
334         return this.macroLibraries;
335     }
336 
337     /**
338      * @see org.andromda.core.templateengine.TemplateEngine#addMacroLibrary(String)
339      */
340     public void addMacroLibrary(String libraryName)
341     {
342         this.macroLibraries.add(libraryName);
343     }
344 
345     /**
346      * @see org.andromda.core.templateengine.TemplateEngine#setMergeLocation(String)
347      */
348     public void setMergeLocation(String mergeLocation)
349     {
350         this.mergeLocation = mergeLocation;
351     }
352 
353     /**
354      * @see org.andromda.core.templateengine.TemplateEngine#shutdown()
355      */
356     public void shutdown()
357     {
358         //  Deletes the merged templates location (these are the templates that were created just for merging
359         //  purposes and so therefore are no longer needed after the engine is shutdown).
360         FileUtils.deleteQuietly(new File(TEMPORARY_TEMPLATE_LOCATION));
361         this.discoveredTemplates.clear();
362         this.velocityEngine = null;
363         if(null!=logger && null!=appender)
364         {
365             logger.removeAppender(appender);
366         }
367     }
368 
369     /**
370      * Opens a log file for this namespace.
371      *
372      * @param pluginName  Name of this plugin
373      * @throws IOException if the file cannot be opened
374      */
375     private void initLogger(final String pluginName)
376         throws IOException
377     {
378         logger = AndroMDALogger.getNamespaceLogger(pluginName);
379         logger.setAdditivity(false);
380 
381         appender =
382             new FileAppender(
383                 new PatternLayout("%-5p %d - %m%n"),
384                 AndroMDALogger.getNamespaceLogFileName(pluginName),
385                 true);
386         logger.addAppender(appender);
387     }
388 }