001// Copyright 2007 by Basil Vandegriend. All rights reserved. 002package com.basilv.envgen; 003 004import java.io.*; 005import java.util.ArrayList; 006import java.util.Iterator; 007import java.util.List; 008import java.util.Map; 009 010import org.apache.log4j.BasicConfigurator; 011import org.apache.log4j.Level; 012import org.apache.log4j.Logger; 013import org.apache.tools.ant.BuildException; 014import org.apache.tools.ant.DirectoryScanner; 015import org.apache.tools.ant.Project; 016import org.apache.tools.ant.Task; 017import org.apache.tools.ant.types.FileSet; 018import org.apache.tools.ant.util.FileUtils; 019 020import com.basilv.core.FileUtilities; 021 022import freemarker.template.Configuration; 023import freemarker.template.Template; 024import freemarker.template.TemplateException; 025import freemarker.template.TemplateModelException; 026 027/** 028 * Ant task for EnvGen - Environment Specific File Generator. 029 */ 030public class EnvGenTask extends Task { 031 032 /** 033 * Represents a shared variable for the FreeMarker configuration. 034 */ 035 public static class SharedVariable { 036 private String name; 037 private String value; 038 039 public String getValue() { 040 return value; 041 } 042 043 public void setValue(String value) { 044 this.value = value; 045 } 046 public String getName() { 047 return name; 048 } 049 public void setName(String name) { 050 this.name = name; 051 } 052 } 053 054 /** 055 * Represents a shared transform for the FreeMarker configuration. 056 */ 057 public static class TransformSpecification { 058 private String name; 059 private String transformClassName; 060 061 // If I try accepting a Class instance rather than a class name, then 062 // testing from ant fails due to ant being unable to instatiante the transform class due 063 // to weird classpath problems. Passing in the class as a string works. 064 public void setClass(String className) { 065 this.transformClassName = className; 066 } 067 public String getName() { 068 return name; 069 } 070 public void setName(String name) { 071 this.name = name; 072 } 073 074 public Object getTransformInstance() { 075 try { 076 Class transformClass = getClass().getClassLoader().loadClass(transformClassName); 077 return transformClass.newInstance(); 078 } catch (Exception e) { 079 throw new RuntimeException("Error creating instance of class [" + transformClassName + "]. " + 080 "The class is likely missing from the classpath.", e); 081 } 082 } 083 } 084 085 // Required 086 private List sourceFileSets = new ArrayList(); 087 088 // Required 089 private File destDir; 090 091 // Required 092 private File envPropertiesFile; 093 094 private List sharedVariables = new ArrayList(); 095 private List transforms = new ArrayList(); 096 097 private boolean overwrite = false; 098 099 private boolean stripFileExtension = false; 100 101 private boolean diffToUpdate = false; 102 103 // TODO: Properties to support later, with initial defaults 104// private boolean flatten = false; 105// private boolean includeEmptyDirs = false; 106 107 108 private int numFilesGenerated; 109 110 // Default no-arg constructor needed by ANT. 111 public EnvGenTask() { 112 113 // Initialize log4j, which freemarker uses, to write to standard output 114 // Initialize log4j here rather than in execute() so that unit tests 115 // also have it initialized if they are calling other methods on this class. 116 117 BasicConfigurator.configure(); 118 119 // Hide debugging output. 120 Logger.getRootLogger().setLevel(Level.INFO); 121 122 } 123 124 public void addConfiguredSource(FileSet fileSet) { 125 sourceFileSets.add(fileSet); 126 } 127 128 public void addConfiguredSharedVariable(SharedVariable var) { 129 sharedVariables.add(var); 130 } 131 132 public void addConfiguredTransform(TransformSpecification transformSpec) { 133 transforms.add(transformSpec); 134 } 135 136 public void setDestDir(File file) { 137 destDir = file; 138 } 139 140 public void setEnvPropertiesFile(File file) { 141 envPropertiesFile = file; 142 } 143 144 public void setOverwrite(boolean overwrite) { 145 this.overwrite = overwrite; 146 } 147 148 public void setDiffToUpdate(boolean diffToUpdate) { 149 this.diffToUpdate = diffToUpdate; 150 } 151 152 public void setStripFileExtension(boolean stripFileExtension) { 153 this.stripFileExtension = stripFileExtension; 154 } 155 156 public void execute() throws BuildException { 157 158 validateSettings(); 159 160 numFilesGenerated = 0; 161 162 // Log settings 163 log("Environment properties file: " + FileUtilities.getCanonicalPath(envPropertiesFile)); 164 165 for (Iterator i = sourceFileSets.iterator(); i.hasNext();) { 166 FileSet fileSet = (FileSet) i.next(); 167 File baseDir = fileSet.getDir(getProject()); 168 log("Source directory: " + FileUtilities.getCanonicalPath(baseDir)); 169 } 170 log("Destination directory: " + FileUtilities.getCanonicalPath(destDir)); 171 172 EnvironmentProperties envProps = EnvironmentPropertiesLoader.load( 173 envPropertiesFile.getPath()); 174 175 Configuration config = createFreemarkerConfiguration(); 176 177 // For each environment 178 for (Iterator envIterator = envProps.getEnvPropertiesList().iterator(); envIterator.hasNext();) { 179 Map propertyMap = (Map) envIterator.next(); 180 181 TemplateMapModel mapModel = new TemplateMapModel(propertyMap); 182 183 String targetDir = generateString(mapModel, FileUtilities.getCanonicalPath(destDir)); 184 185 for (Iterator sourceFileSetIterator = sourceFileSets.iterator(); sourceFileSetIterator.hasNext();) { 186 FileSet fileSet = (FileSet) sourceFileSetIterator.next(); 187 188 File baseDir = fileSet.getDir(getProject()); 189 try { 190 config.setDirectoryForTemplateLoading(baseDir); 191 } catch (IOException e) { 192 throw new RuntimeException("Error generating files from directory [" + 193 FileUtilities.getCanonicalPath(destDir) + "] due to " + e.getMessage() + ".", e); 194 } 195 196 DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject()); 197 String[] includedFilenames = scanner.getIncludedFiles(); 198 199 // For each source file 200 for (int fileIndex = 0; fileIndex < includedFilenames.length; fileIndex++) { 201 String sourceFilename = includedFilenames[fileIndex]; 202 203 String targetFilename = generateString(mapModel, sourceFilename); 204 205 if (stripFileExtension) { 206 targetFilename = stripFileExtension(targetFilename); 207 } 208 209 210 File sourceFile = new File(baseDir, sourceFilename); 211 File targetFile = new File(targetDir, targetFilename); 212 213 // If overwrite is true, then we don't need to bother with the slower diff to update logic. 214 // If we are in diffToUpdate mode and the target file does not exist, we will need to regenerate it 215 if (overwrite || !diffToUpdate || !targetFile.exists()) { 216 if (! shouldGenerateFile(sourceFile, targetFile)) { 217 log("File [" + targetFile.getAbsolutePath() + "] is not generated because it is up to date.", 218 Project.MSG_VERBOSE); 219 continue; 220 } 221 222 223 // Ensure parent directory exists - it may already exist, so do not check return value. 224 targetFile.getParentFile().mkdirs(); 225 226 if (generateFile(config, mapModel, baseDir, sourceFilename, targetFile)) { 227 recordGeneration(sourceFile, targetFile); 228 } 229 230 } else { 231 try { 232 File tempTargetFile = File.createTempFile("EnvGen", ".tmp"); 233 if (generateFile(config, mapModel, baseDir, sourceFilename, tempTargetFile)) { 234 235 FileUtils fileUtils = FileUtils.getFileUtils(); 236 // This comparison ignores line-ending differences, in case the Fixcrlf task 237 // is run on EnvGen's output. 238 if (! fileUtils.contentEquals(targetFile, tempTargetFile, true)) { 239 recordGeneration(sourceFile, targetFile); 240 241 fileUtils.copyFile(tempTargetFile, targetFile, null, true); 242 } 243 } else { 244 FileUtilities.deleteFile(targetFile); 245 } 246 FileUtilities.deleteRecursively(tempTargetFile); 247 248 } catch (IOException e) { 249 throw new RuntimeException("Error creating temp file", e); 250 } 251 252 } 253 } 254 } 255 } 256 257 log("Generated " + numFilesGenerated + " target files."); 258 259 } 260 261 /** 262 * @param sourceFile 263 * @param targetFile 264 */ 265 private void recordGeneration(File sourceFile, File targetFile) { 266 numFilesGenerated++; 267 268 log("Generating from file [" + sourceFile.getAbsolutePath() + "].", Project.MSG_VERBOSE); 269 log(" to file [" + targetFile.getAbsolutePath() + "].", Project.MSG_VERBOSE); 270 } 271 272 273 // Non-private for testing. Assumes that config.setDirectoryForTemplateLoading(sourceBaseDir) was called. 274 // Returns true if file was generated, false if generation was skipped. 275 boolean generateFile(Configuration config, TemplateMapModel mapModel, 276 File sourceBaseDir, String sourceFilename, File targetFile) { 277 278 File sourceFile = new File(sourceBaseDir, sourceFilename); 279 try { 280 281 Template template = config.getTemplate(sourceFilename); 282 Writer outputFileWriter = new BufferedWriter(new FileWriter(targetFile)); 283 try { 284 template.process(mapModel, outputFileWriter); 285 outputFileWriter.flush(); 286 } finally { 287 outputFileWriter.close(); 288 } 289 290 if (SkipGenerationTransform.isSkipGenerationAndResetFlag()) { 291 FileUtilities.deleteFile(targetFile); 292 return false; 293 } 294 295 return true; 296 297 } catch (IOException e) { 298 // Add underlying exception message to wrapper exception message because Ant 299 // only reports the message of the outer exception on the console. 300 throw new RuntimeException("Error producing target file [" + 301 FileUtilities.getCanonicalPath(targetFile) + "] due to " + 302 e.getMessage() + ".", e); 303 304 } catch (TemplateException e) { 305 throw new RuntimeException("Error doing generation from source file [" + 306 FileUtilities.getCanonicalPath(sourceFile) + "] due to " + 307 e.getMessage() + ".", e); 308 } 309 310 } 311 312 // Non-private for testing. 313 static String stripFileExtension(String targetFilename) { 314 if (targetFilename.indexOf(".") == -1) { 315 return targetFilename; 316 } 317 318 int indexOfLastPeriod = targetFilename.lastIndexOf("."); 319 String strippedName = targetFilename.substring(0, indexOfLastPeriod); 320 return strippedName; 321 } 322 323 // Non-private for testing 324 void validateSettings() { 325 if (envPropertiesFile == null) { 326 throw new BuildException("The envPropertiesFile attribute must be specified."); 327 } 328 329 if (!envPropertiesFile.exists()) { 330 throw new BuildException("The environment properties file [" + 331 FileUtilities.getCanonicalPath(envPropertiesFile) + "] does not exist."); 332 } 333 334 if (!envPropertiesFile.isFile()) { 335 throw new BuildException("The environment properties file specified [" + 336 FileUtilities.getCanonicalPath(envPropertiesFile) + "] is not a file."); 337 } 338 339 if (destDir == null) { 340 throw new BuildException("The destDir attribute must be specified."); 341 } 342 // It is okay for the destination directory to not exist, as we will create it. 343 344 if (destDir.exists() && !destDir.isDirectory()) { 345 throw new BuildException("The destination directory specified [" + 346 FileUtilities.getCanonicalPath(destDir) + "] is not a directory."); 347 } 348 349 if (sourceFileSets.isEmpty()) { 350 throw new BuildException("At least one nested <source> element must be specified."); 351 } 352 353 } 354 355 // Non-private for testing 356 int getNumFilesGenerated() { 357 return numFilesGenerated; 358 } 359 360 // Non-private for testing 361 boolean shouldGenerateFile(File sourceFile, File targetFile) { 362 363 if (overwrite) { 364 return true; 365 } 366 367 FileUtils fileUtils = FileUtils.getFileUtils(); 368 369 if (! fileUtils.isUpToDate(sourceFile, targetFile, fileUtils.getFileTimestampGranularity()) ) { 370 return true; 371 } 372 373 if (! fileUtils.isUpToDate(envPropertiesFile, targetFile, fileUtils.getFileTimestampGranularity()) ) { 374 return true; 375 } 376 377 return false; 378 } 379 380 // Non-private for testing 381 Configuration createFreemarkerConfiguration() { 382 Configuration config = new Configuration(); 383 config.setStrictSyntaxMode(true); 384 config.setWhitespaceStripping(true); 385 386 for (Iterator i = sharedVariables.iterator(); i.hasNext(); ) { 387 SharedVariable sharedVariable = (SharedVariable) i.next(); 388 try { 389 // TODO: Convert name to account for periods using TemplateMapModel 390 config.setSharedVariable(sharedVariable.getName(), sharedVariable.getValue()); 391 } catch (TemplateModelException e) { 392 throw new RuntimeException("Error configuring shared variable [" + 393 sharedVariable.getName() + "] due to " + e.getMessage() + ".", e); 394 } 395 } 396 397 for (Iterator i = transforms.iterator(); i.hasNext();) { 398 TransformSpecification transform = (TransformSpecification) i.next(); 399 try { 400 config.setSharedVariable(transform.getName(), transform.getTransformInstance()); 401 } catch (TemplateModelException e) { 402 throw new RuntimeException("Error configuring transform [" + 403 transform.getName() + "] due to " + e.getMessage() + ".", e); 404 } 405 } 406 407 return config; 408 } 409 410 411 // Non-private for testing. 412 String generateString(TemplateMapModel mapModel, String sourceString) { 413 try { 414 Template template = new Template("name", 415 new StringReader(sourceString), createFreemarkerConfiguration()); 416 417 StringWriter targetStringWriter = new StringWriter(); 418 template.process(mapModel, targetStringWriter); 419 targetStringWriter.flush(); 420 421 String targetString = targetStringWriter.getBuffer().toString(); 422 return targetString; 423 } catch (Exception e) { 424 throw new RuntimeException("Error generating from source string [" + 425 sourceString + "] due to " + e.getMessage() + ".", e); 426 } 427 } 428 429}