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}