/*
* Copyright 2013 The Sculptor Project Team, including the original
* author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sculptor.maven.plugin;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
/**
* Base class for all code generator-related {@link Mojo}s of this plugin. It
* provides common properties (like the outlet directories for the code
* generator) and shared code (like handling of the generated files).
*/
public abstract class AbstractGeneratorMojo extends AbstractMojo {
/**
* The enclosing project.
*/
@Parameter(defaultValue="${project}", readonly = true)
protected MavenProject project;
/**
* Directory for source-code artifacts. If an artifact with the same name
* already exists, the generation of the artifact will be skipped.
*/
@Parameter(defaultValue = "src/main/java", required = true)
protected File outletSrcOnceDir;
/**
* Directory for non-source-code artifacts. If an artifact with the same
* name already exists, the generation of the artifact will be skipped.
*/
@Parameter(defaultValue = "src/main/resources", required = true)
protected File outletResOnceDir;
/**
* Directory for source-code artifacts. Existings artifacts will be
* overwritten.
*/
@Parameter(defaultValue = "src/generated/java", required = true)
protected File outletSrcDir;
/**
* Directory for non-source-code artifacts. Existings artifacts will be
* overwritten.
*/
@Parameter(defaultValue = "src/generated/resources", required = true)
protected File outletResDir;
/**
* Directory for Webapp artifacts. If an artifact with the same name already
* exists, the generation of the artifact will be skipped.
*/
@Parameter(defaultValue = "src/main/webapp", required = true)
protected File outletWebrootDir;
/**
* Directory for test source-code artifacts. If an artifact with the same
* name already exists, the generation of the artifact will be skipped.
*/
@Parameter(defaultValue = "src/test/java", required = true)
protected File outletSrcTestOnceDir;
/**
* Directory for test non-source-code artifacts. Existings artifacts will
* not be overwritten.
*/
@Parameter(defaultValue = "src/test/resources", required = true)
protected File outletResTestOnceDir;
/**
* Directory for source-code test-artifacts. Existings artifacts will be
* overwritten.
*/
@Parameter(defaultValue = "src/test/generated/java", required = true)
protected File outletSrcTestDir;
/**
* Directory for non-source-code test-artifacts. Existings artifacts will be
* overwritten.
*/
@Parameter(defaultValue = "src/test/generated/resources", required = true)
protected File outletResTestDir;
/**
* Directory for documentation artifacts. Existings artifacts will be
* overwritten.
*/
@Parameter(defaultValue = "src/site", required = true)
protected File outletDocDir;
/**
* File holding the status of the last code generator execution.
*/
@Parameter(defaultValue = ".sculptor-status", required = true)
private File statusFile;
/**
* Verbose logging.
* <p>
* Can be set from command line using '-Dverbose=true'.
*/
@Parameter(property = "verbose", defaultValue = "false")
private boolean verbose;
/**
* Returns enclosing {@link MavenProject}.
*/
protected MavenProject getProject() {
return project;
}
/**
* Check if the logging should be verbose.
* <p>
* If Maven debug logging is requested "mvn -X" the verbose logging is active
* as well.
*
* @return true to verbose logging
*/
protected boolean isVerbose() {
return verbose || getLog().isDebugEnabled();
}
/**
* Returns the StatusFile (defined via {@link #statusFile}) or
* <code>null</code> if none exists.
*/
protected File getStatusFile() {
return (statusFile.exists() ? statusFile : null);
}
/**
* Updates the StatusFile (defined via {@link #statusFile}). This file
* indicates the last successful execution of the code generator and is used
* to check the state of the source files.
*
* @param createdFiles
* list of files created by the code generator
*/
protected boolean updateStatusFile(List<File> createdFiles) {
boolean success;
final Properties statusProperties = new Properties();
try {
for (File createdFile : createdFiles) {
try {
statusProperties.setProperty(
getProjectRelativePath(createdFile),
calculateChecksum(createdFile));
} catch (IOException e) {
getLog().warn(
"Checksum calculation failed: " + e.getMessage());
}
}
final FileWriter statusWriter = new FileWriter(statusFile);
try {
statusProperties.store(statusWriter,
"Sculptor created the following " + createdFiles.size()
+ " files");
success = true;
} finally {
statusWriter.close();
}
} catch (IOException e) {
getLog().warn("Updating status file failed: " + e.getMessage());
success = false;
}
return success;
}
/**
* Returns the list of generated files from the StatusFile (defined via
* {@link #statusFile}) or <code>null</code> if no StatusFile exists.
*/
protected Set<String> getGeneratedFiles() {
Properties statusFileProps = getStatusProperties();
if (statusFileProps != null) {
return statusFileProps.stringPropertyNames();
}
return null;
}
/**
* Returns the {@link Properties} instance populated from the StatusFile
* (defined via {@link #statusFile}) or <code>null</code> if no StatusFile
* exists.
*/
private Properties getStatusProperties() {
final File statusFile = getStatusFile();
if (statusFile != null) {
try {
Properties statusFileProps = new Properties();
final FileReader statusReader = new FileReader(statusFile);
try {
statusFileProps.load(statusReader);
return statusFileProps;
} finally {
statusReader.close();
}
} catch (IOException e) {
getLog().warn("Reading status file failed: " + e.getMessage());
}
}
return null;
}
/**
* Deletes the files in the directories marked as 'generated' and the
* unmodified one-shot generated files.
* <p>
* The list of all previously generated files is retrieved from the StatusFile
* (defined via {@link #statusFile}). Modified one-shot generated files are
* detected by a changed file checksum.
*/
protected boolean deleteGeneratedFiles() {
boolean success;
// First delete all files in the directories marked as 'generated'
cleanDirectory(outletSrcDir);
cleanDirectory(outletResDir);
cleanDirectory(outletSrcTestDir);
cleanDirectory(outletResTestDir);
// Finally delete the non-modified one-shot generated files in the other folders
Properties statusFileProps = getStatusProperties();
// If there is no status file to compare against or no files are
// generated then skip deletion
if (statusFileProps == null || statusFileProps.isEmpty()) {
// No status file - we can't delete any previously generated files
success = true;
} else {
try {
// Iterate through the list of generated files
for (String fileName : statusFileProps.stringPropertyNames()) {
File file = new File(getProject().getBasedir(), fileName);
if (file.exists()) {
// For one-shot generated files compare checksum before
// deleting
boolean delete;
if (fileName
.startsWith(getProjectRelativePath(outletSrcOnceDir))
|| fileName
.startsWith(getProjectRelativePath(outletResOnceDir))
|| fileName
.startsWith(getProjectRelativePath(outletWebrootDir))
|| fileName
.startsWith(getProjectRelativePath(outletSrcTestOnceDir))
|| fileName
.startsWith(getProjectRelativePath(outletResTestOnceDir))) {
delete = calculateChecksum(file).equals(
statusFileProps.getProperty(fileName));
if (!delete
&& (isVerbose() || getLog()
.isDebugEnabled())) {
getLog().info(
"Keeping previously generated modified"
+ " file: " + file);
}
} else {
delete = false;
}
if (delete) {
if (isVerbose() || getLog().isDebugEnabled()) {
getLog().info(
"Deleting previously generated file: "
+ file);
}
// We have to make sure the file is deleted on
// Windows as well
FileUtils.forceDelete(file);
// Delete image file generated from dot file
if (fileName.endsWith(".dot")) {
File imageFile = new File(getProject()
.getBasedir(), fileName + ".png");
if (imageFile.exists()) {
if (isVerbose()
|| getLog().isDebugEnabled()) {
getLog().info(
"Deleting previously generated file: "
+ imageFile);
}
// We have to make sure the file is deleted
// on Windows as well
FileUtils.forceDelete(imageFile);
}
}
}
}
}
success = true;
} catch (IOException e) {
getLog().warn("Reading status file failed: " + e.getMessage());
success = false;
}
}
return success;
}
/**
* Deletes all files within the given directory.
*/
private void cleanDirectory(File dir) {
if (isVerbose() || getLog().isDebugEnabled()) {
getLog().info("Deleting previously generated files in directory: " + dir.getPath());
}
if (dir.exists()) {
try {
FileUtils.cleanDirectory(dir);
} catch (IOException e) {
getLog().warn("Cleaning directory failed: " + e.getMessage());
}
}
}
/**
* Returns the path of given file relative to the enclosing Maven project.
*/
protected String getProjectRelativePath(File file) {
String path = file.getAbsolutePath();
String prefix = project.getBasedir().getAbsolutePath();
if (path.startsWith(prefix)) {
path = path.substring(prefix.length() + 1);
}
if (File.separatorChar != '/') {
path = path.replace(File.separatorChar, '/');
}
return path;
}
/**
* Returns a hex representation of the MD5 checksum from given file.
*/
private String calculateChecksum(File file) throws IOException {
InputStream is = new FileInputStream(file);
return DigestUtils.md5Hex(is);
}
}