    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    }