package com.github.dzwicker.stjs.gradle;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.file.SourceDirectorySet;
import org.gradle.api.internal.ConventionTask;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.SourceSetOutput;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.util.PatternFilterable;
import org.gradle.api.tasks.util.PatternSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.stjs.generator.GenerationDirectory;
import org.stjs.generator.Generator;
import org.stjs.generator.GeneratorConfiguration;
import org.stjs.generator.GeneratorConfigurationBuilder;
import org.stjs.generator.JavascriptFileGenerationException;
import org.stjs.generator.MultipleFileGenerationException;
import groovy.lang.Closure;
@SuppressWarnings("unused")
public class GenerateStJsTask extends ConventionTask implements PatternFilterable {
private static final Logger logger = LoggerFactory.getLogger(GenerateStJsTask.class);
private static final Object PACKAGE_INFO_JAVA = "package-info.java";
private final PatternFilterable patternSet = new PatternSet();
/**
* The list of packages that can be referenced from the classes that will be processed by the generator
*/
protected List<String> allowedPackages;
/**
* Sets the granularity in milliseconds of the last modification date for testing whether a source needs
* recompilation.<br>
* default-value="0"
*/
private int staleMillis;
/**
* If true the check, if (!array.hasOwnProperty(index)) continue; is added in each "for" array iteration<br>
* default-value="true"
*/
private boolean generateArrayHasOwnProperty = true;
/**
* If true, it generates for each JavaScript the corresponding source map back to the corresponding Java file. It
* also copies the Java source file in the same folder as the generated Javascript file.<br>
* default-value="false"
*/
private boolean generateSourceMap;
/**
* If true, it packs all the generated Javascript file (using the correct dependency order) into a single file named
* artifactName.js<br>
* default-value="false"
*/
protected boolean pack;
/**
* The source directories containing the sources to be compiled.
*/
private SourceDirectorySet compileSourceRoots;
private File generatedSourcesDirectory;
private String encoding = "UTF-8";
private FileCollection classpath;
private boolean war;
private SourceSetOutput output;
public GenerateStJsTask() {
dependsOn(JavaPlugin.CLASSES_TASK_NAME);
setGroup("generate");
}
@TaskAction
protected void generate() {
final GenerationDirectory genDir = getGeneratedSourcesDirectory();
long t1 = System.currentTimeMillis();
logger.info("Generating JavaScript files to " + genDir.getGeneratedSourcesAbsolutePath());
GeneratorConfigurationBuilder configBuilder = new GeneratorConfigurationBuilder();
configBuilder.stjsClassLoader(getBuiltProjectClassLoader());
configBuilder.generationFolder(genDir);
configBuilder.targetFolder(output.getClassesDir());
configBuilder.generateArrayHasOwnProperty(generateArrayHasOwnProperty);
configBuilder.generateSourceMap(generateSourceMap);
configBuilder.sourceEncoding(encoding);
// configBuilder.allowedPackage("org.stjs.javascript");
configBuilder.allowedPackage("org.junit");
// configBuilder.allowedPackage("org.stjs.testing");
if (allowedPackages != null) {
configBuilder.allowedPackages(allowedPackages);
}
// scan all the packages
Collection<String> packages = accumulatePackages();
configBuilder.allowedPackages(packages);
final GeneratorConfiguration configuration = configBuilder.build();
final Generator generator = new Generator(configuration);
final int[] generatedFiles = {0};
final boolean[] hasFailures = new boolean[1];
final File sourceDir = compileSourceRoots.getSrcDirs().iterator().next();
// scan the modified sources
FileTree src = compileSourceRoots.getAsFileTree();
src = src.matching(patternSet);
src.visit(new FileVisitor() {
@Override
public void visitDir(FileVisitDetails dirDetails) {
// ignore
}
@Override
public void visitFile(FileVisitDetails fileDetails) {
if (fileDetails.getName().equals(PACKAGE_INFO_JAVA)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping " + fileDetails);
}
return;
}
File absoluteTarget = new File(
generatedSourcesDirectory,
fileDetails.getRelativePath().getPathString().replaceFirst("\\.java", ".js")
);
if (logger.isDebugEnabled()) {
logger.debug("Generating " + absoluteTarget);
}
if (!absoluteTarget.getParentFile().exists() && !absoluteTarget.getParentFile().mkdirs()) {
logger.error("Cannot create output directory:" + absoluteTarget.getParentFile());
return;
}
String className = getClassNameForSource(fileDetails.getRelativePath().getPathString());
if (logger.isDebugEnabled()) {
logger.debug("Class: " + className);
}
try {
generator.generateJavascript(
className,
sourceDir
);
++generatedFiles[0];
} catch (MultipleFileGenerationException e) {
for (JavascriptFileGenerationException jse : e.getExceptions()) {
logger.error("{}@{},{} has error '{}'",
jse.getSourcePosition().getFile(),
jse.getSourcePosition().getLine(),
jse.getSourcePosition(),
jse.getMessage()
);
logger.error("");
logger.error("");
}
hasFailures[0] = true;
// continue with the next file
} catch (JavascriptFileGenerationException jse) {
logger.error("{}@{},{} has error '{}'",
jse.getSourcePosition().getFile(),
jse.getSourcePosition().getLine(),
jse.getSourcePosition(),
jse.getMessage()
);
logger.error("");
logger.error("");
hasFailures[0] = true;
// continue with the next file
} catch (Exception e) {
// TODO - maybe should filter more here
logger.error("{}@{},{} has error '{}'",
fileDetails.getPath(),
1,
1,
e.toString()
);
logger.error("");
logger.error("");
hasFailures[0] = true;
throw new RuntimeException("Error generating javascript:" + e, e);
}
}
});
generator.close();
long t2 = System.currentTimeMillis();
logger.info("Generated " + generatedFiles[0] + " JavaScript files in " + (t2 - t1) + " ms");
if (generatedFiles[0] > 0) {
filesGenerated(generator);
}
if (hasFailures[0]) {
throw new RuntimeException("Errors generating JavaScript");
}
}
protected void filesGenerated(final Generator generator) {
// copy the javascript support
try {
generator.copyJavascriptSupport(getGeneratedSourcesDirectory().getGeneratedSourcesAbsolutePath());
} catch (Exception ex) {
throw new RuntimeException("Error when copying support files:" + ex.getMessage(), ex);
}
//TODO pack not supported
//packFiles(generator, genDir);
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask include(String... includes) {
patternSet.include(includes);
return this;
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask include(Iterable<String> includes) {
patternSet.include(includes);
return this;
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask include(Spec<FileTreeElement> includeSpec) {
patternSet.include(includeSpec);
return this;
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask include(Closure includeSpec) {
patternSet.include(includeSpec);
return this;
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask exclude(String... excludes) {
patternSet.exclude(excludes);
return this;
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask exclude(Iterable<String> excludes) {
patternSet.exclude(excludes);
return this;
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask exclude(Spec<FileTreeElement> excludeSpec) {
patternSet.exclude(excludeSpec);
return this;
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask exclude(Closure excludeSpec) {
patternSet.exclude(excludeSpec);
return this;
}
/**
* {@inheritDoc}
*/
public Set<String> getIncludes() {
return patternSet.getIncludes();
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask setIncludes(Iterable<String> includes) {
patternSet.setIncludes(includes);
return this;
}
/**
* {@inheritDoc}
*/
public Set<String> getExcludes() {
return patternSet.getExcludes();
}
/**
* {@inheritDoc}
*/
public GenerateStJsTask setExcludes(Iterable<String> excludes) {
patternSet.setExcludes(excludes);
return this;
}
private String getClassNameForSource(String sourcePath) {
// remove ending .java and replace / or \ by .
// (only using File.separatorChar is not enough see https://github.com/dzwicker/st-js-gradle-plugin/issues/3)
return sourcePath.substring(0, sourcePath.length() - 5).replace('/', '.').replace('\\', '.');
}
// //TODO howto handle stale in gradle???
// /**
// * @return the list of Java source files to processed (those which are older than the corresponding Javascript
// * file). The returned files are relative to the given source directory.
// */
// @SuppressWarnings("unchecked")
// private List<File> accumulateSources(GenerationDirectory genDir, File sourceDir, SourceMapping jsMapping, SourceMapping stjsMapping,
// int stale) throws MojoExecutionException {
// final List<File> result = new ArrayList<>();
// if (sourceDir == null || !sourceDir.exists()) {
// return result;
// }
// SourceInclusionScanner jsScanner = getSourceInclusionScanner(stale);
// jsScanner.addSourceMapping(jsMapping);
//
// SourceInclusionScanner stjsScanner = getSourceInclusionScanner(stale);
// stjsScanner.addSourceMapping(stjsMapping);
//
// final Set<File> staleFiles = new LinkedHashSet<>();
//
// for (File f : sourceDir.listFiles()) {
// if (!f.isDirectory()) {
// continue;
// }
//
// try {
// staleFiles.addAll(jsScanner.getIncludedSources(f.getParentFile(), genDir.getAbsolutePath()));
// staleFiles.addAll(stjsScanner.getIncludedSources(f.getParentFile(), getBuildOutputDirectory()));
// } catch (InclusionScanException e) {
// throw new MojoExecutionException("Error scanning source root: \'" + sourceDir.getPath() + "\' "
// + "for stale files to recompile.", e);
// }
// }
//
// // Trim root path from file paths
// for (File file : staleFiles) {
// String filePath = file.getPath();
// String basePath = sourceDir.getAbsoluteFile().toString();
// result.add(new File(filePath.substring(basePath.length() + 1)));
// }
// return result;
// }
//
// protected SourceInclusionScanner getSourceInclusionScanner(int staleMillis) {
// SourceInclusionScanner scanner;
//
// if (includes.isEmpty() && excludes.isEmpty()) {
// scanner = new StaleClassSourceScanner(staleMillis, getBuildOutputDirectory());
// } else {
// if (includes.isEmpty()) {
// includes.add("**/*.java");
// }
// scanner = new StaleClassSourceScanner(staleMillis, includes, excludes, getBuildOutputDirectory());
// }
//
// return scanner;
// }
private Collection<String> accumulatePackages() {
final Collection<String> result = new HashSet<>();
compileSourceRoots.getAsFileTree().visit(new FileVisitor() {
@Override
public void visitDir(FileVisitDetails dirDetails) {
final String packageName =
dirDetails.getRelativePath().getPathString().replace(File.separatorChar, '.');
if (logger.isDebugEnabled()) {
logger.debug("Packages: " + packageName);
}
result.add(packageName);
}
@Override
public void visitFile(FileVisitDetails fileDetails) {
// ignore
}
});
return result;
}
private ClassLoader getBuiltProjectClassLoader() {
final List<URL> urls = new ArrayList<>();
classpath.getAsFileTree().visit(new FileVisitor() {
@Override
public void visitDir(FileVisitDetails dirDetails) {
//ignore
}
@Override
public void visitFile(FileVisitDetails fileDetails) {
if (logger.isDebugEnabled()) {
logger.debug("Classpath: " + fileDetails.getFile());
}
try {
urls.add(fileDetails.getFile().toURI().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException("Error trying to set the Hibernate Tools classpath", e);
}
}
});
try {
urls.add(output.getClassesDir().toURI().toURL());
urls.add(output.getResourcesDir().toURI().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException("Error trying to set the Hibernate Tools classpath", e);
}
return new URLClassLoader(
urls.toArray(new URL[] {}),
Thread.currentThread().getContextClassLoader()
);
}
public GenerationDirectory getGeneratedSourcesDirectory() {
final File classpath = null;
final File relativeToClasspath = new File("/");
return new GenerationDirectory(generatedSourcesDirectory, classpath, relativeToClasspath.toURI());
}
public List<String> getAllowedPackages() {
return allowedPackages;
}
public void setAllowedPackages(List<String> allowedPackages) {
this.allowedPackages = allowedPackages;
}
public int getStaleMillis() {
return staleMillis;
}
public void setStaleMillis(int staleMillis) {
this.staleMillis = staleMillis;
}
public boolean isGenerateArrayHasOwnProperty() {
return generateArrayHasOwnProperty;
}
public void setGenerateArrayHasOwnProperty(boolean generateArrayHasOwnProperty) {
this.generateArrayHasOwnProperty = generateArrayHasOwnProperty;
}
public boolean isGenerateSourceMap() {
return generateSourceMap;
}
public void setGenerateSourceMap(boolean generateSourceMap) {
this.generateSourceMap = generateSourceMap;
}
public boolean isPack() {
return pack;
}
public void setPack(boolean pack) {
this.pack = pack;
}
public SourceDirectorySet getCompileSourceRoots() {
return compileSourceRoots;
}
public void setCompileSourceRoots(SourceDirectorySet compileSourceRoots) {
this.compileSourceRoots = compileSourceRoots;
}
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Returns the classpath
*
* @return a {@link FileCollection} representing all classpath elements
*/
@InputFiles
public FileCollection getClasspath() {
return classpath;
}
/**
* Set the classpath
*
* @param classpath a {@link FileCollection} to overwrite the actual classpath elements
*/
public void setClasspath(final FileCollection classpath) {
this.classpath = classpath;
}
/**
* Set the classpath
*
* @param classpath a {@link FileCollection} to overwrite the actual classpath elements
*/
public void classpath(final FileCollection classpath) {
this.classpath = classpath;
}
public boolean isWar() {
return war;
}
public void setWar(boolean war) {
this.war = war;
}
public void setGeneratedSourcesDirectory(File generatedSourcesDirectory) {
this.generatedSourcesDirectory = generatedSourcesDirectory;
}
public void setOutput(SourceSetOutput output) {
this.output = output;
}
public SourceSetOutput getOutput() {
return output;
}
}