org.clickframes.util.CodeGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.clickframes.util.CodeGenerator.java

Source

/*
 * Clickframes: Full lifecycle software development automation.
 * Copyright (C)  2009 Children's Hospital Boston
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.clickframes.util;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.clickframes.VelocityHelper;
import org.clickframes.techspec.Techspec;

/**
 * @author Vineet Manohar
 */
public class CodeGenerator {
    @SuppressWarnings("unused")
    private final Log logger = LogFactory.getLog(getClass());
    private static final Pattern TEMPLATE_PATTERN = Pattern.compile("^(.*)/([^/]+).vm$");
    private static final Pattern POM_PATTERN = Pattern.compile("^([^/]+).vm$");
    private static final Log log = LogFactory.getLog(CodeGenerator.class);

    /**
     * No-op constructor for now.
     */
    public CodeGenerator() {
        // No-op constructor for now.
    }

    /**
     *
     * @param params
     *            map of substitution variables
     * @param realDir
     *            code should be generated in this directory for creation only,
     *            no overwrite allowed
     * @param targetDir
     *            this is the fallback directory, overwriting existing file is
     *            allowed in this directory
     * @param templateName
     *            the template to be used to generated code
     * @param filename
     *            the name of the output file
     *
     * @author Vineet Manohar
     * @return
     * @throws IOException
     */
    File generateCode(CodeOverwritePolicy policy, Map<String, Object> params, File realDir, File targetDir,
            String templateName, String filename) throws IOException {
        File generatedFile = File.createTempFile("clickframes-", filename);
        generatedFile.deleteOnExit();

        VelocityHelper.runMacro(params, templateName, generatedFile);

        File realFile = new File(realDir, filename);
        File targetFile = new File(targetDir, filename);
        File destinationFile;

        switch (policy) {
        case OVERWRITE_ALLOWED:
            destinationFile = realFile;
            break;

        case OVERWRITE_NOT_ALLOWED:
            destinationFile = targetFile;
            break;

        case OVERWRITE_IF_UNCHANGED:
            if (!realFile.exists()) {
                destinationFile = realFile;
                break;
            }

            if (!modifiedSinceLastGeneration(realFile)) {
                // quit, if the new file is 'logically' same as the next
                // file. e.g. if
                // only white space changes have been made
                if (generatedFileLogicallySameAsExistingFile(generatedFile, realFile)) {
                    return realFile;
                }

                destinationFile = realFile;
                break;
            }

            destinationFile = targetFile;
            break;
        default:
            throw new RuntimeException(
                    "Developer exception: CodeOverwritePolicy not implemented for this policy: " + policy);
        }

        if (log.isDebugEnabled() && destinationFile.equals(realFile)) {
            if (realFile.exists()) {
                // log.debug(filename + " updated, being replaced.");
                // log.debug(realFile.getAbsolutePath() +
                // " updated, being replaced.");
            } else {
                // log.debug(filename + " created.");
            }
        }

        destinationFile.getParentFile().mkdirs();

        // cannot rename to an existing file with windows
        if (destinationFile.exists()) {
            destinationFile.delete();
        }

        storeChecksumInFile(generatedFile);
        FileUtils.copyFile(generatedFile, destinationFile);

        return destinationFile;
    }

    /**
     * use case: the user formats the white-space of existing file. the
     * generated file is defined to be the same as the existing file if the only
     * changes made to the existing file is white space and formatting related,
     * such that our checksum algorithm computes the same checksum
     *
     * @param generatedFileWithoutChecksum
     * @param existingUnmodifiedFileWithChecksum
     * @return
     *
     * @author Vineet Manohar
     * @throws IOException
     */
    private boolean generatedFileLogicallySameAsExistingFile(File generatedFileWithoutChecksum,
            File existingUnmodifiedFileWithChecksum) throws IOException {
        // existing file with checksum
        String existingFileWithChecksumAsString = FileUtils.readFileToString(existingUnmodifiedFileWithChecksum);

        // is checksum string present
        Pattern pattern = Pattern.compile(".*clickframes::(version=(\\d+))::clickframes.*", Pattern.DOTALL);
        Matcher m = pattern.matcher(existingFileWithChecksumAsString);
        if (!m.matches()) {
            throw new IllegalArgumentException(
                    "File does not have checksum stored: " + existingUnmodifiedFileWithChecksum);
        }
        long checksumFoundInFile = Long.parseLong(m.group(2));

        // generated file without checksum
        String generatedContentWithoutChecksum = FileUtils.readFileToString(generatedFileWithoutChecksum);
        long checksumOfGeneratedContent = checksum(generatedContentWithoutChecksum);

        return checksumFoundInFile == checksumOfGeneratedContent;
    }

    public boolean modifiedSinceLastGeneration(File file) throws IOException {
        String fileAsString = FileUtils.readFileToString(file);
        // is checksum string present
        Pattern pattern = Pattern.compile(".*clickframes::(version=(\\d+))::clickframes.*", Pattern.DOTALL);
        Matcher m = pattern.matcher(fileAsString);
        if (!m.matches()) {
            // no match found, can't guarentee that it has not changed
            return true;
        }
        long checksumFoundInFile = Long.parseLong(m.group(2));

        // calculate checksum of the remaining file
        String remainingFile = removeCommentForFilename(file.getName(), m.group(1), fileAsString);
        long checksum = checksum(remainingFile);
        return checksum != checksumFoundInFile;
    }

    public static void storeChecksumInFile(File file) throws IOException {
        long checksum = checksum(FileUtils.readFileToString(file));

        @SuppressWarnings("unchecked")
        List<String> lines = FileUtils.readLines(file);

        // add checksum at the end of the file
        lines.add(commentForFilename(file.getName(), "version=" + checksum));

        FileUtils.writeLines(file, lines);
    }

    private static String commentForFilename(String filename, String comment) {
        if (filename.endsWith(".xml") || filename.endsWith(".xhtml") || filename.endsWith(".htm")
                || filename.endsWith(".html") || filename.endsWith(".php")) {
            return "<!-- clickframes::" + comment + "::clickframes -->";
        }
        if (filename.endsWith(".java")) {
            return "// clickframes::" + comment + "::clickframes";
        }
        if (filename.endsWith(".js")) {
            return "/* clickframes::" + comment + "::clickframes */";
        }
        if (filename.endsWith(".properties")) {
            return "# clickframes::" + comment + "::clickframes";
        }
        if (filename.endsWith(".jsp")) {
            return "<%-- clickframes::" + comment + "::clickframes--%>";
        }
        if (filename.endsWith(".txt")) {
            return "";
        }
        if (filename.endsWith(".css")) {
            return "/* " + comment + " */";
        }
        if (filename.endsWith(".vm")) {
            return "## " + comment;
        }
        if (filename.endsWith(".htaccess")) {
            return "# " + comment;
        }

        throw new RuntimeException("File extension not supported: " + filename);
    }

    private static long checksum(String text) throws IOException {
        File file = File.createTempFile("clickframes-", ".tmp");
        FileUtils.writeStringToFile(file, normalizeWhitespaces(text));
        return FileUtils.checksumCRC32(file);
    }

    private static String normalizeWhitespaces(String text) {
        return text.replaceAll("\\s+", " ").trim();
    }

    private static String removeCommentForFilename(String filename, String comment, String text) {
        String commentForFile = commentForFilename(filename, comment);
        return text.replaceAll(commentForFile, "");
    }

    public void generateCodeOrArtifact(boolean artifactOld, Map<String, Object> params, String inputPath,
            String outputDirectory, String outputFilename) throws IOException {
        Techspec techspec = (Techspec) params.get("techspec");

        if (techspec.getOutputDirectory() == null) {
            throw new RuntimeException(
                    "Techspec must be configured correctly to generated code! OutputDirectory is null", null);
        }

        File targetDirectory = new File(techspec.getOutputDirectory(),
                ClickframeUtils.convertSlashToPathSeparator("target/clickframes-modified" + outputDirectory));
        File realDirectory;

        realDirectory = new File(techspec.getOutputDirectory(),
                ClickframeUtils.convertSlashToPathSeparator(outputDirectory));

        CodeOverwritePolicy codeOverwritePolicy = CodeOverwritePolicy.OVERWRITE_IF_UNCHANGED;
        generateCode(codeOverwritePolicy, params, realDirectory, targetDirectory, inputPath, outputFilename);
    }

    public void generateCode(String inputPath, String outputDirectory, String outputFilename, Object... parameters)
            throws IOException {
        Map<String, Object> params = convertParametersToMap(parameters);
        generateCodeOrArtifact(false, params, inputPath, outputDirectory, outputFilename);
    }

    // Am I really needed?
    public void generateCodeAutoscan(String inputPath, String outputDirectory, String outputFilename,
            Map<String, Object> params) throws IOException {

        generateCodeOrArtifact(false, params, inputPath, outputDirectory, outputFilename);
    }

    public void generateArtifact(String inputPath, String outputDirectory, String outputFilename,
            Object... parameters) throws IOException {
        Map<String, Object> params = convertParametersToMap(parameters);

        generateCodeOrArtifact(true, params, inputPath, outputDirectory, outputFilename);
    }

    /**
     * the template name is used to determine the name of the output file
     *
     * @throws IOException
     */
    public void generateCode(String templatePath, Object... parameters) throws IOException {
        // String filename = null;
        // String relativePath = null;

        // if template is "src/main/webapp/index.html.vm"
        // filename index.html
        // relative path is src/main/webapp
        Matcher m = TEMPLATE_PATTERN.matcher(templatePath);
        if (m.matches()) {
            String relativePath = m.group(1);
            String filename = m.group(2);
            generateCode(templatePath, relativePath, filename, parameters);
            return;
        }

        // if template is "pom.xml.vm"
        // filename pom.xml
        // relative path is ""
        m = POM_PATTERN.matcher(templatePath);
        if (m.matches()) {
            String relativePath = ".";
            String filename = m.group(1);
            generateCode(templatePath, relativePath, filename, parameters);
            return;
        }

        throw new RuntimeException("Invalid format for template path: " + templatePath
                + ", example pattern is 'src/main/webapp/index.html.vm' - must end in .vm");
    }

    /**
     * @param parameters
     *            an alternating list of key value pairs, e.g. {"key1", value1,
     *            "key2", value2} all keys are Strings
     *
     * @return
     *
     * @author Vineet Manohar
     */
    private static Map<String, Object> convertParametersToMap(Object[] parameters) {
        if (parameters.length % 2 == 1) {
            throw new RuntimeException("Name value pair list must be also even in number, found: "
                    + parameters.length + ": " + parameters);
        }
        Map<String, Object> params = new HashMap<String, Object>();
        for (int i = 0; i < parameters.length; i += 2) {
            Object key = parameters[i];
            Object value = parameters[i + 1];
            if (!(key instanceof String)) {
                throw new RuntimeException("Names passed to code generator must always be of type String, found "
                        + key.getClass() + " (" + key + ")");
            }
            params.put((String) key, value);
        }

        return params;
    }
}