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}