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    }