package net.jangaroo.jooc.mvnplugin;
import net.jangaroo.jooc.AbstractCompileLog;
import net.jangaroo.jooc.Jooc;
import net.jangaroo.jooc.api.CompilationResult;
import net.jangaroo.jooc.config.DebugMode;
import net.jangaroo.jooc.config.JoocConfiguration;
import net.jangaroo.jooc.config.PublicApiViolationsMode;
import net.jangaroo.jooc.config.SemicolonInsertionMode;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.compiler.CompilerError;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Super class for mojos compiling Jangaroo sources.
*/
@SuppressWarnings({"UnusedDeclaration", "UnusedPrivateField"})
public abstract class AbstractCompilerMojo extends JangarooMojo {
/**
* The Maven project object
*
* @parameter expression="${project}"
*/
private MavenProject project;
/**
* Indicates whether the build will fail if there are compilation errors.
*
* @parameter expression="${maven.compiler.failOnError}" default-value="true"
*/
@SuppressWarnings("FieldCanBeLocal")
private boolean failOnError = true;
/**
* Set "enableAssertions" to "true" in order to generate runtime checks for assert statements.
*
* @parameter expression="${maven.compile.ea}" default-value="false"
*/
private boolean enableAssertions;
/**
* Set "allowDuplicateLocalVariables" to "true" in order to allow multiple declarations of local variables
* within the same scope.
*
* @parameter default-value="false"
*/
private boolean allowDuplicateLocalVariables;
/**
* "publicApiViolations" controls how the compiler reacts on usage of non-public API classes,
* i.e. classes annotated with <code>[ExcludeClass]</code>.
* It can take the values "warn" to log a warning whenever such a class is used, "allow" to suppress such warnings,
* and "error" to stop the build with an error.
*
* @parameter default-value="warn"
*/
private String publicApiViolations;
/**
* If set to "true", the compiler will add an [ExcludeClass] annotation to any
* API stub whose source class contains neither an [PublicApi] nor an [ExcludeClass]
* annotation.
*
* @parameter expression="${maven.compiler.excludeClassByDefault}" default-value="false"
*/
private boolean excludeClassByDefault;
/**
* If set to "true", the compiler will generate more detailed progress information.
*
* @parameter expression="${maven.compiler.verbose}" default-value="false"
*/
private boolean verbose;
/**
* Sets the granularity in milliseconds of the last modification
* date for testing whether a source needs recompilation.
*
* @parameter expression="${lastModGranularityMs}" default-value="0"
*/
private int staleMillis;
/**
* Keyword list to be appended to the -g command-line switch. Legal values are one of the following keywords: none, lines, or source.
* If debuglevel is not specified, by default, nothing will be appended to -g. If debug is not turned on, this attribute will be ignored.
*
* @parameter default-value="source"
*/
private String debuglevel;
/**
* Keyword list to be appended to the -autosemicolon command-line switch. Legal values are one of the following keywords: error, warn (default), or quirks.
*
* @parameter default-value="warn"
*/
private String autoSemicolon;
/**
* Source directory to scan for files to compile.
*
* @parameter expression="${project.build.sourceDirectory}"
*/
protected File sourceDirectory;
/**
* Output directory for all generated ActionScript3 files to compile.
*
* @parameter expression="${project.build.directory}/generated-sources/joo"
*/
private File generatedSourcesDirectory;
@Override
protected MavenProject getProject() {
return project;
}
public abstract String getModuleClassesJsFileName();
public File getModuleClassesJsFile() {
return new File(getOutputDirectory(), getModuleClassesJsFileName());
}
protected abstract List<File> getCompileSourceRoots();
protected abstract File getOutputDirectory();
protected File getClassesOutputDirectory() {
return new File(getOutputDirectory(), "joo/classes");
}
protected abstract File getTempClassesOutputDirectory();
public File getGeneratedSourcesDirectory() {
return generatedSourcesDirectory;
}
protected abstract File getApiOutputDirectory();
/**
* Runs the compile mojo
*
* @throws MojoExecutionException
* @throws MojoFailureException
*/
public void execute() throws MojoExecutionException, MojoFailureException {
final Log log = getLog();
if (getCompileSourceRoots().isEmpty()) {
log.info("No sources to compile");
return;
}
// ----------------------------------------------------------------------
// Create the compiler configuration
// ----------------------------------------------------------------------
JoocConfiguration configuration = new JoocConfiguration();
configuration.setEnableAssertions(enableAssertions);
configuration.setAllowDuplicateLocalVariables(allowDuplicateLocalVariables);
configuration.setVerbose(verbose);
configuration.setExcludeClassByDefault(excludeClassByDefault);
if (StringUtils.isNotEmpty(debuglevel)) {
try {
configuration.setDebugMode(DebugMode.valueOf(debuglevel.toUpperCase()));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("The specified debug level: '" + debuglevel
+ "' is unsupported. " + "Legal values are 'none', 'lines', and 'source'.");
}
}
if (StringUtils.isNotEmpty(autoSemicolon)) {
try {
configuration.setSemicolonInsertionMode(SemicolonInsertionMode.valueOf(autoSemicolon.toUpperCase()));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("The specified semicolon insertion mode: '" + autoSemicolon
+ "' is unsupported. " + "Legal values are 'error', 'warn', and 'quirks'.");
}
}
if (StringUtils.isNotEmpty(publicApiViolations)) {
try {
configuration.setPublicApiViolationsMode(PublicApiViolationsMode.valueOf(publicApiViolations.toUpperCase()));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("The specified public API violations mode: '" + publicApiViolations
+ "' is unsupported. " + "Legal values are 'error', 'warn', and 'allow'.");
}
}
HashSet<File> sources = new HashSet<File>();
log.debug("starting source inclusion scanner");
sources.addAll(computeStaleSources(staleMillis));
if (sources.isEmpty()) {
log.info("Nothing to compile - all classes are up to date");
return;
}
configuration.setSourceFiles(new ArrayList<File>(sources));
try {
configuration.setSourcePath(getCompileSourceRoots());
} catch (IOException e) {
throw new MojoFailureException("could not canonicalize source paths: " + getCompileSourceRoots(), e);
}
configuration.setClassPath(getActionScriptClassPath());
configuration.setOutputDirectory(getClassesOutputDirectory());
configuration.setApiOutputDirectory(getApiOutputDirectory());
if (log.isDebugEnabled()) {
log.debug("Source path: " + configuration.getSourcePath().toString().replace(',', '\n'));
log.debug("Class path: " + configuration.getClassPath().toString().replace(',', '\n'));
log.debug("Output directory: " + configuration.getOutputDirectory());
if (configuration.getApiOutputDirectory() != null) {
log.debug("API output directory: " + configuration.getApiOutputDirectory());
}
}
int result = compile(configuration);
boolean compilationError = (result != CompilationResult.RESULT_CODE_OK);
if (!compilationError) {
// for now, always set debug mode to "false" for concatenated file:
configuration.setDebugMode(null);
configuration.setOutputDirectory(getTempClassesOutputDirectory());
configuration.setApiOutputDirectory(null);
result = compile(configuration);
if (result == CompilationResult.RESULT_CODE_OK) {
buildOutputFile(getTempClassesOutputDirectory(), getModuleClassesJsFile());
}
compilationError = (result != CompilationResult.RESULT_CODE_OK);
}
List<CompilerError> messages = Collections.emptyList();
if (compilationError && failOnError) {
log.info("-------------------------------------------------------------");
log.error("COMPILATION ERROR : ");
log.info("-------------------------------------------------------------");
if (messages != null) {
for (CompilerError message : messages) {
log.error(message.toString());
}
log.info(messages.size() + ((messages.size() > 1) ? " errors " : "error"));
log.info("-------------------------------------------------------------");
}
throw new MojoFailureException("Compilation failed");
} else {
for (CompilerError message : messages) {
log.warn(message.toString());
}
}
}
protected abstract List<File> getActionScriptClassPath();
private void buildOutputFile(File tempOutputDir, File outputFile) throws MojoExecutionException {
final Log log = getLog();
if (log.isDebugEnabled()) {
log.debug("Output file: " + outputFile);
}
try {
// If the directory where the output file is going to land
// doesn't exist then create it.
File outputFileDirectory = outputFile.getParentFile();
if (!outputFileDirectory.exists()) {
//noinspection ResultOfMethodCallIgnored
if (outputFileDirectory.mkdirs()) {
log.debug("created output directory " + outputFileDirectory);
}
}
@SuppressWarnings({"unchecked"})
// resource bundle classes should always be loaded dynamically:
List<File> files = FileUtils.getFiles(tempOutputDir, "**/*.js", "**/*_properties_*.js");
// We should now have all the files we want to concat so let's do it.
Writer fos = new OutputStreamWriter(new FileOutputStream(outputFile), "UTF-8");
int tempOutputDirPathLength = tempOutputDir.getAbsolutePath().length() + 1;
for (File file : files) {
String className = file.getAbsolutePath();
className = className.substring(tempOutputDirPathLength, className.length() - ".js".length());
className = className.replace(File.separatorChar, '.');
fos.write("// class " + className + "\n");
IOUtil.copy(new FileInputStream(file), fos, "UTF-8");
fos.write('\n');
}
fos.close();
} catch (IOException e) {
throw new MojoExecutionException("could not build output file " + outputFile + ": " + e.toString(), e);
}
}
private int compile(JoocConfiguration config) throws MojoExecutionException {
File outputDirectory = config.getOutputDirectory();
// create output directory if it does not exist
if (!outputDirectory.exists()) {
if (!outputDirectory.mkdirs()) {
throw new MojoExecutionException("Failed to create output directory " + outputDirectory.getAbsolutePath());
}
}
// create api output directory if it does not exist
File apiOutputDirectory = getApiOutputDirectory();
if (apiOutputDirectory != null && !apiOutputDirectory.exists()) {
if (!apiOutputDirectory.mkdirs()) {
throw new MojoExecutionException("Failed to create api output directory " + apiOutputDirectory.getAbsolutePath());
}
}
final List<File> sources = config.getSourceFiles();
final Log log = getLog();
log.info("Compiling " + sources.size() +
" joo source file"
+ (sources.size() == 1 ? "" : "s")
+ " to " + outputDirectory);
Jooc jooc = new Jooc(config, new AbstractCompileLog() {
@Override
protected void doLogError(String msg) {
log.error(msg);
}
@Override
public void warning(String msg) {
log.warn(msg);
}
});
return jooc.run().getResultCode();
}
private List<File> computeStaleSources(int staleMillis) throws MojoExecutionException {
File outputDirectory = getClassesOutputDirectory();
List<File> compileSourceRoots = getCompileSourceRoots();
return getMavenPluginHelper().computeStaleSources(compileSourceRoots, getIncludes(), getExcludes(), outputDirectory, Jooc.INPUT_FILE_SUFFIX, Jooc.OUTPUT_FILE_SUFFIX, staleMillis);
}
protected abstract Set<String> getIncludes();
protected abstract Set<String> getExcludes();
protected boolean isJangarooPackaging() {
return Types.JANGAROO_TYPE.equals(getProject().getPackaging());
}
}