    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                                                                    tempTargetFile.delete();
    243                                                            }
    244                                                    } else {
    245                                                            targetFile.delete();
    246                                                    }
    247                                                    } catch (IOException e) {
    248                                                            throw new RuntimeException("Error creating temp file", e);
    249                                                    }
    250
    251                                            }
    252                                    }
    253                            }
    254                    }
    255
    256                    log("Generated " + numFilesGenerated + " target files.");
    257
    258            }
    259
    260            /**
    261             * @param sourceFile
    262             * @param targetFile
    263             */
    264            private void recordGeneration(File sourceFile, File targetFile) {
    265                    numFilesGenerated++;
    266
    267                    log("Generating from file [" + sourceFile.getAbsolutePath() + "].", Project.MSG_VERBOSE);
    268                    log("   to file [" + targetFile.getAbsolutePath() + "].", Project.MSG_VERBOSE);
    269            }
    270
    271
    272            // Non-private for testing. Assumes that config.setDirectoryForTemplateLoading(sourceBaseDir) was called.
    273            // Returns true if file was generated, false if generation was skipped.
    274            boolean generateFile(Configuration config, TemplateMapModel mapModel,
    275                    File sourceBaseDir, String sourceFilename, File targetFile) {
    276
    277                    File sourceFile = new File(sourceBaseDir, sourceFilename);
    278                    try {
    279
    280                            Template template = config.getTemplate(sourceFilename);
    281                            Writer outputFileWriter = new BufferedWriter(new FileWriter(targetFile));
    282                            try {
    283                                    template.process(mapModel, outputFileWriter);
    284                                    outputFileWriter.flush();
    285                            } finally {
    286                                    outputFileWriter.close();
    287                            }
    288
    289                            if (SkipGenerationTransform.isSkipGenerationAndResetFlag()) {
    290                                    targetFile.delete();
    291                                    return false;
    292                            }
    293
    294                            return true;
    295
    296                    } catch (IOException e) {
    297                            // Add underlying exception message to wrapper exception message because Ant
    298                            // only reports the message of the outer exception on the console.
    299                            throw new RuntimeException("Error producing target file [" +
    300                                    FileUtilities.getCanonicalPath(targetFile) + "] due to " +
    301                                    e.getMessage() + ".", e);
    302
    303                    } catch (TemplateException e) {
    304                            throw new RuntimeException("Error doing generation from source file [" +
    305                                    FileUtilities.getCanonicalPath(sourceFile) + "] due to " +
    306                                    e.getMessage() + ".", e);
    307                    }
    308
    309            }
    310
    311            // Non-private for testing.
    312            static String stripFileExtension(String targetFilename) {
    313                    if (targetFilename.indexOf(".") == -1) {
    314                            return targetFilename;
    315                    }
    316
    317                    int indexOfLastPeriod = targetFilename.lastIndexOf(".");
    318                    String strippedName = targetFilename.substring(0, indexOfLastPeriod);
    319                    return strippedName;
    320            }
    321
    322            // Non-private for testing
    323            void validateSettings() {
    324                    if (envPropertiesFile == null) {
    325                            throw new BuildException("The envPropertiesFile attribute must be specified.");
    326                    }
    327
    328                    if (!envPropertiesFile.exists()) {
    329                            throw new BuildException("The environment properties file [" +
    330                                    FileUtilities.getCanonicalPath(envPropertiesFile) + "] does not exist.");
    331                    }
    332
    333                    if (!envPropertiesFile.isFile()) {
    334                            throw new BuildException("The environment properties file specified [" +
    335                                    FileUtilities.getCanonicalPath(envPropertiesFile) + "] is not a file.");
    336                    }
    337
    338                    if (destDir == null) {
    339                            throw new BuildException("The destDir attribute must be specified.");
    340                    }
    341                    // It is okay for the destination directory to not exist, as we will create it.
    342
    343                    if (destDir.exists() && !destDir.isDirectory()) {
    344                            throw new BuildException("The destination directory specified [" +
    345                                    FileUtilities.getCanonicalPath(destDir) + "] is not a directory.");
    346                    }
    347
    348                    if (sourceFileSets.isEmpty()) {
    349                            throw new BuildException("At least one nested <source> element must be specified.");
    350                    }
    351
    352            }
    353
    354            // Non-private for testing
    355            int getNumFilesGenerated() {
    356                    return numFilesGenerated;
    357            }
    358
    359            // Non-private for testing
    360            boolean shouldGenerateFile(File sourceFile, File targetFile) {
    361
    362                    if (overwrite) {
    363                            return true;
    364                    }
    365
    366                    FileUtils fileUtils = FileUtils.getFileUtils();
    367
    368                    if (! fileUtils.isUpToDate(sourceFile, targetFile, fileUtils.getFileTimestampGranularity()) ) {
    369                            return true;
    370                    }
    371
    372                    if (! fileUtils.isUpToDate(envPropertiesFile, targetFile, fileUtils.getFileTimestampGranularity()) ) {
    373                            return true;
    374                    }
    375
    376                    return false;
    377            }
    378
    379            // Non-private for testing
    380            Configuration createFreemarkerConfiguration() {
    381                    Configuration config = new Configuration();
    382                    config.setStrictSyntaxMode(true);
    383                    config.setWhitespaceStripping(true);
    384
    385                    for (Iterator i = sharedVariables.iterator(); i.hasNext(); ) {
    386                            SharedVariable sharedVariable = (SharedVariable) i.next();
    387                            try {
    388                                    // TODO: Convert name to account for periods using TemplateMapModel
    389                                    config.setSharedVariable(sharedVariable.getName(), sharedVariable.getValue());
    390                            } catch (TemplateModelException e) {
    391                                    throw new RuntimeException("Error configuring shared variable [" +
    392                                            sharedVariable.getName() + "] due to " + e.getMessage() + ".", e);
    393                            }
    394                    }
    395
    396                    for (Iterator i = transforms.iterator(); i.hasNext();) {
    397                            TransformSpecification transform = (TransformSpecification) i.next();
    398                            try {
    399                                    config.setSharedVariable(transform.getName(), transform.getTransformInstance());
    400                            } catch (TemplateModelException e) {
    401                                    throw new RuntimeException("Error configuring transform [" +
    402                                            transform.getName() + "] due to " + e.getMessage() + ".", e);
    403                            }
    404                    }
    405
    406                    return config;
    407            }
    408
    409
    410            // Non-private for testing.
    411        String generateString(TemplateMapModel mapModel, String sourceString) {
    412            try {
    413                Template template = new Template("name",
    414                    new StringReader(sourceString), createFreemarkerConfiguration());
    415
    416                StringWriter targetStringWriter = new StringWriter();
    417                template.process(mapModel, targetStringWriter);
    418                targetStringWriter.flush();
    419
    420                String targetString = targetStringWriter.getBuffer().toString();
    421                return targetString;
    422            } catch (Exception e) {
    423                    throw new RuntimeException("Error generating from source string [" +
    424                            sourceString + "] due to " + e.getMessage() + ".", e);
    425            }
    426        }
    427
    428    }