001 // Copyright 2007 by Basil Vandegriend. All rights reserved.
002 package com.basilv.envgen;
003
004 import java.io.*;
005 import java.util.ArrayList;
006 import java.util.Iterator;
007 import java.util.List;
008 import java.util.Map;
009
010 import org.apache.log4j.BasicConfigurator;
011 import org.apache.log4j.Level;
012 import org.apache.log4j.Logger;
013 import org.apache.tools.ant.BuildException;
014 import org.apache.tools.ant.DirectoryScanner;
015 import org.apache.tools.ant.Project;
016 import org.apache.tools.ant.Task;
017 import org.apache.tools.ant.types.FileSet;
018 import org.apache.tools.ant.util.FileUtils;
019
020 import com.basilv.core.FileUtilities;
021
022 import freemarker.template.Configuration;
023 import freemarker.template.Template;
024 import freemarker.template.TemplateException;
025 import freemarker.template.TemplateModelException;
026
027 /**
028 * Ant task for EnvGen - Environment Specific File Generator.
029 */
030 public 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 }