001: package org.andromda.templateengines.velocity;
002:
003: import java.io.File;
004: import java.io.IOException;
005: import java.io.InputStream;
006: import java.io.StringWriter;
007: import java.io.Writer;
008: import java.net.URL;
009: import java.util.ArrayList;
010: import java.util.Collection;
011: import java.util.HashMap;
012: import java.util.Iterator;
013: import java.util.List;
014: import java.util.Map;
015: import java.util.Properties;
016:
017: import org.andromda.core.common.AndroMDALogger;
018: import org.andromda.core.common.Constants;
019: import org.andromda.core.common.ExceptionUtils;
020: import org.andromda.core.common.Merger;
021: import org.andromda.core.common.ResourceUtils;
022: import org.andromda.core.common.ResourceWriter;
023: import org.andromda.core.templateengine.TemplateEngine;
024: import org.andromda.core.templateengine.TemplateEngineException;
025: import org.apache.commons.collections.ExtendedProperties;
026: import org.apache.commons.lang.StringUtils;
027: import org.apache.log4j.FileAppender;
028: import org.apache.log4j.Logger;
029: import org.apache.log4j.PatternLayout;
030: import org.apache.velocity.Template;
031: import org.apache.velocity.VelocityContext;
032: import org.apache.velocity.app.VelocityEngine;
033: import org.apache.velocity.runtime.RuntimeServices;
034: import org.apache.velocity.runtime.log.LogSystem;
035: import org.apache.velocity.runtime.resource.loader.FileResourceLoader;
036:
037: /**
038: * The TemplateEngine implementation for VelocityTemplateEngine template processor.
039: *
040: * @author <a href="http://www.mbohlen.de">Matthias Bohlen </a>
041: * @author Chad Brandon
042: * @see http://jakarta.apache.org/velocity/
043: */
044: public class VelocityTemplateEngine implements TemplateEngine {
045: protected static Logger logger = null;
046:
047: /**
048: * The directory we look in to find velocity properties.
049: */
050: private static final String PROPERTIES_DIR = "META-INF/";
051:
052: /**
053: * The suffix for the the velocity properties.
054: */
055: private static final String PROPERTIES_SUFFIX = "-velocity.properties";
056:
057: /**
058: * The location to which temporary templates are written
059: */
060: private static final String TEMPORARY_TEMPLATE_LOCATION = Constants.TEMPORARY_DIRECTORY
061: + "velocity/merged";
062:
063: /**
064: * The location of external templates
065: */
066: private String mergeLocation;
067:
068: /**
069: * Stores additional properties specified within the plugin within the file META-INF/'plugin
070: * name'-velocity.properties
071: */
072: private Properties properties = null;
073:
074: /**
075: * The current namespace this template engine is running within.
076: */
077: private String namespace;
078:
079: /**
080: * the VelocityEngine instance to use
081: */
082: private VelocityEngine velocityEngine;
083: private VelocityContext velocityContext;
084: private final List macroLibraries = new ArrayList();
085:
086: /**
087: * Stores a collection of templates that have already been
088: * discovered by the velocity engine
089: */
090: private final Map discoveredTemplates = new HashMap();
091:
092: /**
093: * Stores the merged template files that are deleted at shutdown.
094: */
095: private final Collection mergedTemplateFiles = new ArrayList();
096:
097: /**
098: * @see org.andromda.core.templateengine.TemplateEngine#init(java.lang.String)
099: */
100: public void initialize(final String namespace) throws Exception {
101: this .namespace = namespace;
102: this .initLogger(namespace);
103:
104: ExtendedProperties engineProperties = new ExtendedProperties();
105:
106: // Tell VelocityTemplateEngine it should also use the
107: // classpath when searching for templates
108: // IMPORTANT: file,andromda.plugins the ordering of these
109: // two things matters, the ordering allows files to override
110: // the resources found on the classpath.
111: engineProperties.setProperty(VelocityEngine.RESOURCE_LOADER,
112: "file,classpath");
113:
114: engineProperties.setProperty("file."
115: + VelocityEngine.RESOURCE_LOADER + ".class",
116: FileResourceLoader.class.getName());
117:
118: engineProperties.setProperty("classpath."
119: + VelocityEngine.RESOURCE_LOADER + ".class",
120: ClasspathResourceLoader.class.getName());
121:
122: // Tell VelocityTemplateEngine not to use its own logger
123: // the logger but to use of this plugin.
124: engineProperties.setProperty(
125: VelocityEngine.RUNTIME_LOG_LOGSYSTEM,
126: new VelocityLoggingReceiver());
127:
128: // Let this template engine know about the macro libraries.
129: for (final Iterator iterator = getMacroLibraries().iterator(); iterator
130: .hasNext();) {
131: engineProperties.addProperty(VelocityEngine.VM_LIBRARY,
132: iterator.next());
133: }
134:
135: this .velocityEngine = new VelocityEngine();
136: this .velocityEngine.setExtendedProperties(engineProperties);
137:
138: if (this .mergeLocation != null) {
139: // set the file resource path (to the merge location)
140: velocityEngine.addProperty(
141: VelocityEngine.FILE_RESOURCE_LOADER_PATH,
142: this .mergeLocation);
143: }
144:
145: // if the namespace requires a merge add the temporary template
146: // location to which merged templates are written
147: if (Merger.instance().requiresMerge(this .namespace)) {
148: velocityEngine.addProperty(
149: VelocityEngine.FILE_RESOURCE_LOADER_PATH, this
150: .getMergedTemplatesLocation());
151: }
152:
153: this .addProperties(namespace);
154: this .velocityEngine.init();
155: }
156:
157: /**
158: * Adds any properties found within META-INF/'plugin name'-velocity.properties
159: */
160: private void addProperties(String pluginName) throws IOException {
161: // reset any properties from previous processing
162: this .properties = null;
163:
164: // see if the velocity properties exist for the current
165: // plugin
166: URL propertiesUri = ResourceUtils.getResource(PROPERTIES_DIR
167: + StringUtils.trimToEmpty(pluginName)
168: + PROPERTIES_SUFFIX);
169:
170: if (propertiesUri != null) {
171: if (logger.isDebugEnabled()) {
172: logger.debug("loading properties from --> '"
173: + propertiesUri + "'");
174: }
175:
176: this .properties = new Properties();
177: this .properties.load(propertiesUri.openStream());
178:
179: for (final Iterator iterator = this .properties.keySet()
180: .iterator(); iterator.hasNext();) {
181: final String property = (String) iterator.next();
182: final String value = this .properties
183: .getProperty(property);
184: if (logger.isDebugEnabled()) {
185: logger.debug("setting property '" + property
186: + "' with --> '" + value + "'");
187: }
188: this .velocityEngine.setProperty(property, value);
189: }
190: }
191: }
192:
193: /**
194: * @see org.andromda.core.templateengine.TemplateEngine#processTemplate(java.lang.String, java.util.Map,
195: * java.io.StringWriter)
196: */
197: public void processTemplate(final String templateFile,
198: final Map templateObjects, final Writer output)
199: throws Exception {
200: final String methodName = "VelocityTemplateEngine.processTemplate";
201:
202: if (logger.isDebugEnabled()) {
203: logger
204: .debug("performing " + methodName
205: + " with templateFile '" + templateFile
206: + "' and templateObjects '"
207: + templateObjects + "'");
208: }
209: ExceptionUtils.checkEmpty("templateFile", templateFile);
210: ExceptionUtils.checkNull("output", output);
211: this .velocityContext = new VelocityContext();
212: this .loadVelocityContext(templateObjects);
213:
214: Template template = (Template) this .discoveredTemplates
215: .get(templateFile);
216: if (template == null) {
217: template = this .velocityEngine.getTemplate(templateFile);
218:
219: // We check to see if the namespace requires a merge, and if so
220: final Merger merger = Merger.instance();
221: if (merger.requiresMerge(this .namespace)) {
222: final String mergedTemplateLocation = this
223: .getMergedTemplateLocation(templateFile);
224: final InputStream resource = template
225: .getResourceLoader().getResourceStream(
226: templateFile);
227: ResourceWriter.instance().writeStringToFile(
228: merger
229: .getMergedString(resource,
230: this .namespace),
231: mergedTemplateLocation);
232: template = this .velocityEngine
233: .getTemplate(templateFile);
234: this .mergedTemplateFiles.add(new File(
235: mergedTemplateLocation));
236: }
237: this .discoveredTemplates.put(templateFile, template);
238: }
239: template.merge(velocityContext, output);
240: }
241:
242: /**
243: * Loads the internal {@link #velocityContext} from the
244: * given Map of template objects.
245: *
246: * @param a Map containing objects to add to the template context.
247: */
248: private final void loadVelocityContext(final Map templateObjects) {
249: if (templateObjects != null && !templateObjects.isEmpty()) {
250: // copy the templateObjects to the velocityContext
251: if (templateObjects != null) {
252: for (final Iterator namesIterator = templateObjects
253: .keySet().iterator(); namesIterator.hasNext();) {
254: final String name = (String) namesIterator.next();
255: final Object value = templateObjects.get(name);
256: this .velocityContext.put(name, value);
257: }
258: }
259: }
260: }
261:
262: /**
263: * Gets location to which the given <code>templateName</code>
264: * has its merged output written.
265: * @param templatePath the relative path to the template.
266: * @return the complete merged template location.
267: */
268: private String getMergedTemplateLocation(String templatePath) {
269: return this .getMergedTemplatesLocation() + "/" + templatePath;
270: }
271:
272: /**
273: * Gets the location to which merge templates are written. These
274: * must be written in order to replace the unmerged ones when Velocity
275: * performs its template search.
276: *
277: * @return the merged templates location.
278: */
279: private String getMergedTemplatesLocation() {
280: return TEMPORARY_TEMPLATE_LOCATION + "/" + this .namespace;
281: }
282:
283: /**
284: * The log tag used for evaluation (this can be any abitrary name).
285: */
286: private static final String LOG_TAG = "logtag";
287:
288: /**
289: * @see org.andromda.core.templateengine.TemplateEngine#getEvaluatedExpression(java.lang.String, java.util.Map)
290: */
291: public String getEvaluatedExpression(final String expression,
292: final Map templateObjects) {
293: String evaluatedExpression = null;
294: if (StringUtils.isNotEmpty(expression)
295: && templateObjects != null
296: && !templateObjects.isEmpty()) {
297: if (this .velocityContext == null) {
298: this .velocityContext = new VelocityContext();
299: this .loadVelocityContext(templateObjects);
300: }
301: try {
302: final StringWriter writer = new StringWriter();
303: this .velocityEngine.evaluate(this .velocityContext,
304: writer, LOG_TAG, expression);
305: evaluatedExpression = writer.toString();
306: } catch (final Throwable throwable) {
307: throw new TemplateEngineException(throwable);
308: }
309: }
310: return evaluatedExpression;
311: }
312:
313: /**
314: * @see org.andromda.core.templateengine.TemplateEngine#getMacroLibraries()
315: */
316: public List getMacroLibraries() {
317: return this .macroLibraries;
318: }
319:
320: /**
321: * @see org.andromda.core.templateengine.TemplateEngine#addMacroLibrary(java.lang.String)
322: */
323: public void addMacroLibrary(String libraryName) {
324: this .macroLibraries.add(libraryName);
325: }
326:
327: /**
328: * @see org.andromda.core.templateengine.TemplateEngine#setMergeLocation(java.lang.String)
329: */
330: public void setMergeLocation(String mergeLocation) {
331: this .mergeLocation = mergeLocation;
332: }
333:
334: /**
335: * @see org.andromda.core.templateengine.TemplateEngine#shutdown()
336: */
337: public void shutdown() {
338: this .deleteMergedTemplatesLocation();
339: this .discoveredTemplates.clear();
340: this .velocityEngine = null;
341: }
342:
343: /**
344: * Deletes the merged templates location (these
345: * are the templates that were created just for merging
346: * purposes and so therefore are no longer needed after
347: * the engine is shutdown).
348: */
349: private final void deleteMergedTemplatesLocation() {
350: File directory = new File(TEMPORARY_TEMPLATE_LOCATION);
351: if (directory.getParentFile().isDirectory()) {
352: directory = directory.getParentFile();
353: ResourceUtils.deleteDirectory(directory);
354: directory.delete();
355: }
356: }
357:
358: /**
359: * Opens a log file for this namespace.
360: *
361: * @throws IOException if the file cannot be opened
362: */
363: private final void initLogger(final String pluginName)
364: throws IOException {
365: logger = AndroMDALogger.getNamespaceLogger(pluginName);
366: logger.setAdditivity(false);
367:
368: final FileAppender appender = new FileAppender(
369: new PatternLayout("%-5p %d - %m%n"), AndroMDALogger
370: .getNamespaceLogFileName(pluginName), true);
371: logger.addAppender(appender);
372: }
373:
374: /**
375: * <p/>
376: * This class receives log messages from VelocityTemplateEngine and forwards them to the concrete logger that is
377: * configured for this cartridge. </p>
378: * <p/>
379: * This avoids creation of one large VelocityTemplateEngine log file where errors are difficult to find and track.
380: * </p>
381: * <p/>
382: * Error messages can now be traced to plugin activities. </p>
383: */
384: private static class VelocityLoggingReceiver implements LogSystem {
385: /**
386: * @see org.apache.velocity.runtime.log.LogSystem#init(org.apache.velocity.runtime.RuntimeServices)
387: */
388: public void init(RuntimeServices services) throws Exception {
389: }
390:
391: /**
392: * @see org.apache.velocity.runtime.log.LogSystem#logVelocityMessage(int, java.lang.String)
393: */
394: public void logVelocityMessage(int level, String message) {
395: switch (level) {
396: case LogSystem.WARN_ID:
397: logger.info(message);
398: break;
399: case LogSystem.INFO_ID:
400: logger.info(message);
401: break;
402: case LogSystem.DEBUG_ID:
403: logger.debug(message);
404: break;
405: case LogSystem.ERROR_ID:
406: logger.info(message);
407: break;
408: default:
409: logger.debug(message);
410: break;
411: }
412: }
413: }
414: }
|