    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}