001package org.andromda.templateengines.velocity;
002
003import java.io.File;
004import java.io.IOException;
005import java.io.InputStream;
006import java.io.StringWriter;
007import java.io.Writer;
008import java.net.URL;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.HashMap;
012import java.util.List;
013import java.util.Map;
014import java.util.Properties;
015import org.andromda.core.common.AndroMDALogger;
016import org.andromda.core.common.Constants;
017import org.andromda.core.common.ExceptionUtils;
018import org.andromda.core.common.Merger;
019import org.andromda.core.common.ResourceUtils;
020import org.andromda.core.common.ResourceWriter;
021import org.andromda.core.templateengine.TemplateEngine;
022import org.andromda.core.templateengine.TemplateEngineException;
023import org.apache.commons.collections.ExtendedProperties;
024import org.apache.commons.io.FileUtils;
025import org.apache.commons.lang.StringUtils;
026import org.apache.log4j.FileAppender;
027import org.apache.log4j.Logger;
028import org.apache.log4j.PatternLayout;
029import org.apache.velocity.Template;
030import org.apache.velocity.VelocityContext;
031import org.apache.velocity.app.VelocityEngine;
032import org.apache.velocity.runtime.RuntimeConstants;
033import org.apache.velocity.runtime.log.Log4JLogChute;
034import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
035import org.apache.velocity.runtime.resource.loader.FileResourceLoader;
036import org.apache.velocity.tools.generic.EscapeTool;
037
038/**
039 * The TemplateEngine implementation for VelocityTemplateEngine template processor.
040 *
041 * @author <a href="http://www.mbohlen.de">Matthias Bohlen </a>
042 * @author Chad Brandon
043 * @see "http://jakarta.apache.org/velocity/"
044 */
045public class VelocityTemplateEngine
046    implements TemplateEngine
047{
048    /**
049     * Log4J logger
050     */
051    protected static Logger logger = null;
052
053    /**
054     * Log4J appender
055     */
056    protected FileAppender appender = null;
057
058    /**
059     * The directory we look in to find velocity properties.
060     */
061    private static final String PROPERTIES_DIR = "META-INF/";
062
063    /**
064     * The suffix for the the velocity properties.
065     */
066    private static final String PROPERTIES_SUFFIX = "-velocity.properties";
067
068    /**
069     * The location to which temporary templates are written
070     */
071    private static final String TEMPORARY_TEMPLATE_LOCATION = Constants.TEMPORARY_DIRECTORY + "velocity/merged";
072
073    /**
074     * The location of external templates
075     */
076    private String mergeLocation;
077
078    /**
079     * The current namespace this template engine is running within.
080     */
081    private String namespace;
082
083    /**
084     * The VelocityEngine instance to use
085     */
086    private VelocityEngine velocityEngine;
087    /**
088     * The VelocityContext instance to use
089     */
090    private VelocityContext velocityContext;
091    /**
092     * The Macro Libraries
093     */
094    private final List<String> macroLibraries = new ArrayList<String>();
095
096    /**
097     * Stores a collection of templates that have already been
098     * discovered by the velocity engine
099     */
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}