/*
* 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.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.FileSet;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
import org.sculptor.generator.SculptorGeneratorIssue;
import org.sculptor.generator.SculptorGeneratorResult;
import org.sculptor.generator.SculptorGeneratorResult.Status;
import org.sculptor.generator.SculptorGeneratorRunner;
import org.sonatype.plexus.build.incremental.BuildContext;
/**
* This plugin starts the Sculptor code generator by launching an Eclipse MWE2
* workflow.
* <p>
* You can configure resources that should be checked if they are up-to-date to
* avoid needless generator runs and optimize build execution time.
*/
@Mojo(name = "generate", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true)
public class GeneratorMojo extends AbstractGeneratorMojo {
protected static final String OUTPUT_SLOT_PATH_PREFIX = "outputSlot.path.";
/**
* The current build session instance. This is used for toolchain manager
* API calls.
*/
@Parameter(defaultValue="${session}", readonly = true)
private MavenSession session;
/**
* Eclipse M2E integration.
*/
@Component
private BuildContext buildContext;
/**
* Relative path of model file.
*/
@Parameter(defaultValue = "src/main/resources/model.btdesign", required = true)
private String model;
/**
* A <code>java.util.List</code> of {@link FileSet}s that will be checked on
* up-to-date. If all resources are up-to-date the plugin stops the
* execution, because there are no files to regenerate. <br/>
* The entries of this list can be relative path to the project root or
* absolute path.
* <p>
* If not specified then a fileset with the default value of
* <code>"src/main/resources/*.btdesign"</code> is used.
*/
@Parameter
private FileSet[] checkFileSets;
/**
* Skip the execution.
* <p>
* Can be set from command line using '-Dsculptor.generator.skip=true'.
*/
@Parameter(property = "sculptor.generator.skip", defaultValue = "false")
private boolean skip;
/**
* Don't try to detect if Sculptor code generation is up-to-date and can be
* skipped.
* <p>
* Can be set from command line using '-Dsculptor.generator.force=true'.
*/
@Parameter(property = "sculptor.generator.force", defaultValue = "false")
private boolean force;
/**
* Delete all previously generated files before starting code generation.
* <p>
* Can be set from command line using '-Dsculptor.generator.clean=false'.
*/
@Parameter(property = "sculptor.generator.clean", defaultValue = "true")
private boolean clean;
/**
* Properties used to define system properties (like
* <code>"sculptor.generatorPropertiesLocation"</code>) or to overrride the
* settings retrieved from
* <code>"default-sculptor-generator.properties"</code>.
* <p>
* <b>Sample:</b>
*
* <pre>
* <properties>
* <sculptor.generatorPropertiesLocation>${basedir}/src/sculptor/generator.properties<sculptor.generatorPropertiesLocation>
* </properties>
* </pre>
*/
@Parameter
private Map<String, String> properties;
/**
* Returns <code>model</code> file.
*/
protected File getModelFile() {
return new File(project.getBasedir(), model);
}
/**
* Check if the execution should be skipped.
*
* @return true to skip
*/
protected boolean isSkip() {
return skip;
}
/**
* Check if the execution should be forced.
*
* @return true to force
*/
protected boolean isForce() {
return force;
}
/**
* Check if the previously generated files should be deleted before starting
* the code generator.
*
* @return true to delete
*/
protected boolean isClean() {
return clean;
}
/**
* Strategy implementation of running the code generator:
* <ol>
* <li>check the <code>skip</code> flag
* <li>check the specified <code>workflowDescriptor</code> file
* <li>get a list of modified files from <code>checkFileSets</code>
* <li>run the code generator
* <li>update the <code>statusFile</code>
* <li>extend the compile roots and resources of the enclosing Maven project
* with the output directories of the code generator
* </ol>
*/
public final void execute() throws MojoExecutionException {
// Initialize missing Mojo parameters
initMojoMultiValueParameters();
// If skip flag set then omit code generator execution
if (isSkip()) {
getLog().info("Skipping code generator execution");
return;
}
// Check model file
File modelFile = getModelFile();
if (!modelFile.exists() || !modelFile.isFile()) {
throw new MojoExecutionException("Model file '" + model + "' specified in <model/> does not exists");
}
// If not forced flag set the check for modified source files
Set<String> changedFiles;
if (isForce()) {
getLog().info("Forced code generator execution");
changedFiles = null;
} else {
changedFiles = getChangedFiles();
}
// Code generator is only executed if force flag is set or source files
// are modified
if (changedFiles == null || !changedFiles.isEmpty()) {
// If clean flag set then delete the previously generated files
if (isClean()) {
deleteGeneratedFiles();
} else {
getLog().info("Automatic cleanup disabled - keeping " + "previously generated files");
}
// Execute Sculptor code generator
if (!executeGenerator()) {
throw new MojoExecutionException("Sculptor code generator failed");
}
}
// Extend the Maven projects compile source roots and resource
// directories with the generators directories
extendProjectCompileRootsAndResources();
}
/**
* Initialize default values for the multi-value Mojo parameter
* <code>checkFileSets</code>.
*/
protected void initMojoMultiValueParameters() {
// Set default values for 'checkFileSets' to "src/main/resources/*.btdesign"
if (checkFileSets == null) {
FileSet defaultFileSet = new FileSet();
defaultFileSet.setDirectory(project.getBasedir() + "/src/main/resources");
defaultFileSet.addInclude("*.btdesign");
checkFileSets = new FileSet[] { defaultFileSet };
}
}
/**
* Extends {@link MavenProject}s compile source roots and resource
* directories with the directories holding the generated artifacts.
* <p>
* There's no problem to call this method multiple time. The corresponding
* directories are added only once.
*/
protected void extendProjectCompileRootsAndResources() throws MojoExecutionException {
if (project != null) {
try {
extendCompileSourceRoots();
extendResources(outletResDir, false);
extendResources(outletResOnceDir, false);
extendResources(outletResTestDir, true);
extendResources(outletResTestOnceDir, true);
} catch (Exception ex) {
throw new MojoExecutionException("Could not extend the " + "projects compile paths", ex);
}
}
}
/**
* Extends {@link MavenProject}s <code>CompileSourceRoots</code> and
* <code>TestCompileSourceRoots</code> with the directories holding the
* generated source code artifacts.
* <p>
* There's no problem in adding the same directory multiple times because
* this is handled by {@link MavenProject}.
*/
private void extendCompileSourceRoots() {
// Add source artifacts
if (isVerbose()) {
getLog().info("Adding compile source directory '" + outletSrcDir + "'");
}
project.addCompileSourceRoot(outletSrcDir.getAbsolutePath());
if (isVerbose()) {
getLog().info("Adding compile source directory '" + outletSrcOnceDir + "'");
}
project.addCompileSourceRoot(outletSrcOnceDir.getAbsolutePath());
// Add test source artifacts
if (isVerbose()) {
getLog().info("Adding test compile source directory '" + outletSrcTestDir + "'");
}
project.addTestCompileSourceRoot(outletSrcTestDir.getAbsolutePath());
if (isVerbose()) {
getLog().info("Adding test compile source directory '" + outletSrcTestOnceDir + "'");
}
project.addTestCompileSourceRoot(outletSrcTestOnceDir.getAbsolutePath());
}
/**
* Extends {@link MavenProject}s <code>Resources</code> and
* <code>TestResources</code> (depending on <code>isTest</code>) with the
* directories holding the generated resource artifacts.
* <p>
* There's no problem in adding the same directory multiple times.
*
* @param resourceDir
* resource directory to be added
* @param isTest
* if <code>true</code> then then given directory is added to the
* test resources otherwise it's added to the source resources
*/
private void extendResources(File resourceDir, boolean isTest) {
List<Resource> resources = (isTest ? project.getTestResources() : project.getResources());
// Check if resource directory is already added
for (Resource resource : resources) {
if (resource.getDirectory().equalsIgnoreCase(resourceDir.getAbsolutePath())) {
return;
}
}
// Add new resource to list of resources
if (isVerbose()) {
getLog().info("Adding resource directory '" + resourceDir + "'");
}
Resource resource = new Resource();
resource.setDirectory(resourceDir.getAbsolutePath());
resources.add(resource);
}
/**
* Returns a list with file names from the <code>checkFileSets</code>
* parameter that have been modified since previous generator run. Empty if
* no files changed or <code>null</code> if there is no status file to
* compare against, i.e. always run the generator.
*/
protected Set<String> getChangedFiles() {
// If there is no status file to compare against then always run the
// generator
File statusFile = getStatusFile();
if (statusFile == null) {
return null;
}
final long statusFileLastModified = statusFile.lastModified();
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if (isVerbose()) {
getLog().info("Last code generator execution: " + df.format(new Date(statusFileLastModified)));
}
// Create list of files to check - start with project pom.xml and
// generator config files
List<File> filesToCheck = new ArrayList<File>();
filesToCheck.add(new File(project.getBasedir(), "pom.xml"));
// Now add generator config files
FileSet generatorFileSet = new FileSet();
generatorFileSet.setDirectory(project.getBasedir() + "/src/main/resources/generator");
generatorFileSet.addInclude("*");
filesToCheck.addAll(toFileList(generatorFileSet));
// Finally add files from configured filesets in 'checkFileSets'
for (FileSet fs : checkFileSets) {
filesToCheck.addAll(toFileList(fs));
}
// Check files for modification
Set<String> changedFiles = new HashSet<String>();
for (File checkFile : filesToCheck) {
if (!checkFile.isAbsolute()) {
checkFile = new File(project.getBasedir(), checkFile.getPath());
}
final boolean isModified = checkFile.lastModified() > statusFileLastModified;
if (isModified) {
changedFiles.add(checkFile.getAbsolutePath());
}
if (isVerbose()) {
getLog().info(
"File '" + checkFile.getAbsolutePath() + "': " + (isModified ? "outdated" : "up-to-date")
+ " (" + " " + df.format(new Date(checkFile.lastModified())) + ")");
}
}
// Print info of result
if (changedFiles.size() == 1) {
String fileName = changedFiles.iterator().next();
if (fileName.startsWith(project.getBasedir().getAbsolutePath())) {
fileName = fileName.substring(project.getBasedir().getAbsolutePath().length() + 1);
}
final String message = MessageFormat.format("\"{0}\" has been modified since last generator "
+ "run at {1}", fileName, df.format(new Date(statusFileLastModified)));
getLog().info(message);
} else if (changedFiles.size() > 1) {
final String message = MessageFormat.format("{0} checked resources have been modified since "
+ "last generator run at {1}", changedFiles.size(), df.format(new Date(statusFileLastModified)));
getLog().info(message);
} else {
getLog().info("Everything is up-to-date - no generator run is needed");
}
return changedFiles;
}
/**
* Executes the commandline running the Eclipse MWE2 launcher and returns
* the commandlines exit value.
*/
protected boolean executeGenerator() throws MojoExecutionException {
// Add resources and output directory to plugins classpath
List<Object> classpathEntries = new ArrayList<Object>();
classpathEntries.addAll(project.getResources());
classpathEntries.add(project.getBuild().getOutputDirectory());
extendPluginClasspath(classpathEntries);
// Prepare properties for the code generator
Properties generatorProperties = new Properties();
// Set properties defined properties in the plugins
if (properties != null) {
for (String key : properties.keySet()) {
generatorProperties.setProperty(key, properties.get(key));
}
}
// Set properties with output slot paths
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_SRC", outletSrcOnceDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_RESOURCES", outletResOnceDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_GEN_SRC", outletSrcDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_GEN_RESOURCES", outletResDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_WEBROOT", outletWebrootDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_SRC_TEST", outletSrcTestOnceDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_RESOURCES_TEST", outletResTestOnceDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_GEN_SRC_TEST", outletSrcTestDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_GEN_RESOURCES_TEST", outletResTestDir.toString());
generatorProperties.setProperty(OUTPUT_SLOT_PATH_PREFIX + "TO_DOC", outletDocDir.toString());
// Execute commandline and retrieve list of generated files
List<File> generatedFiles = doRunGenerator(generatorProperties);
if (generatedFiles != null) {
// If the code generation succeeded then write status file (and refresh Eclipse workspace) else delete generated files
if (isVerbose()) {
for (File generatedFile : generatedFiles) {
getLog().info("Generated: " + getProjectRelativePath(generatedFile));
}
}
updateStatusFile(generatedFiles);
if (generatedFiles.size() > 0) {
refreshEclipseWorkspace();
}
getLog().info("Generated " + generatedFiles.size() + " files");
return true;
} else {
getLog().error("Executing generator workflow failed");
}
return false;
}
private void refreshEclipseWorkspace() {
buildContext.refresh(outletSrcOnceDir);
buildContext.refresh(outletResOnceDir);
buildContext.refresh(outletSrcDir);
buildContext.refresh(outletResDir);
buildContext.refresh(outletWebrootDir);
buildContext.refresh(outletSrcTestOnceDir);
buildContext.refresh(outletResTestOnceDir);
buildContext.refresh(outletSrcTestDir);
buildContext.refresh(outletResTestDir);
buildContext.refresh(outletDocDir);
}
protected List<File> doRunGenerator(Properties generatorProperties) {
SculptorGeneratorResult result = SculptorGeneratorRunner.run(getModelFile(), generatorProperties);
// Log all issues occured during workflow execution
for (SculptorGeneratorIssue issue : result.getIssues()) {
switch (issue.getSeverity()) {
case ERROR :
if (issue.getThrowable() != null) {
getLog().error(issue.getMessage(), issue.getThrowable());
} else {
getLog().error(issue.getMessage());
}
break;
case WARNING :
getLog().warn(issue.getMessage());
break;
case INFO :
getLog().info(issue.getMessage());
break;
}
}
// Abort build on error
return (result.getStatus() == Status.SUCCESS ? result.getGeneratedFiles() : null);
}
public void extendPluginClasspath(List<Object> classpathEntries) throws MojoExecutionException {
// we need a LinkedHashSet here to preserve insertion order
Set<URL> urls = new LinkedHashSet<URL>();
for (Object classpathEntry : classpathEntries) {
if (classpathEntry instanceof Resource) {
File resourceFile = new File(((Resource) classpathEntry).getDirectory());
if (resourceFile.exists()) {
getLog().debug("Adding resource to plugin classpath: " + resourceFile.getPath());
try {
urls.add(resourceFile.toURI().toURL());
} catch (MalformedURLException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
} else if (classpathEntry instanceof String) {
File path = new File((String) classpathEntry);
if (path.exists()) {
getLog().debug("Adding path to plugin classpath: " + path.getPath());
try {
urls.add(path.toURI().toURL());
} catch (MalformedURLException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
} else {
throw new IllegalArgumentException("Unsupported classpathentry: " + classpathEntry);
}
}
ClassLoader classLoader = new ResourceChildFirstURLClassLoader(urls, Thread.currentThread()
.getContextClassLoader());
getLog().debug("Setting new context classloader '" + classLoader + "': " + urls);
Thread.currentThread().setContextClassLoader(classLoader);
}
@SuppressWarnings("unchecked")
private List<File> toFileList(FileSet fileSet) {
File directory = new File(fileSet.getDirectory());
String includes = toCommaSeparatedString(fileSet.getIncludes());
String excludes = toCommaSeparatedString(fileSet.getExcludes());
try {
return FileUtils.getFiles(directory, includes, excludes);
} catch (IllegalStateException e) {
getLog().warn(e.getMessage() + ". Ignoring fileset.");
} catch (IOException e) {
getLog().warn(e);
}
return Collections.emptyList();
}
private String toCommaSeparatedString(Collection<String> strings) {
StringBuilder sb = new StringBuilder();
for (String string : strings) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(string);
}
return sb.toString();
}
}