All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.stjs.maven.AbstractSTJSMojo Maven / Gradle / Ivy

/**
 * Copyright 2011 Alexandru Craciun, Eyal Kaspi
 * 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.stjs.maven;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.compiler.util.scan.InclusionScanException;
import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
import org.codehaus.plexus.compiler.util.scan.mapping.SourceMapping;
import org.codehaus.plexus.compiler.util.scan.mapping.SuffixMapping;
import org.codehaus.plexus.util.DirectoryScanner;
import org.jgrapht.DirectedGraph;
import org.jgrapht.alg.StrongConnectivityInspector;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.traverse.TopologicalOrderIterator;
import org.sonatype.plexus.build.incremental.BuildContext;
import org.stjs.generator.BridgeClass;
import org.stjs.generator.ClassWithJavascript;
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 org.stjs.generator.STJSClass;
import org.stjs.generator.name.DependencyType;

import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.google.debugging.sourcemap.SourceMapFormat;
import com.google.debugging.sourcemap.SourceMapGeneratorFactory;
import com.google.debugging.sourcemap.SourceMapGeneratorV3;

/**
 * This is the Maven plugin that launches the Javascript generator. The plugin needs a list of packages containing the Java classes that will
 * processed to generate the corresponding Javascript classes. The Javascript files are generated in a configured target folder.
 * @author Alexandru Craciun
 */
abstract public class AbstractSTJSMojo extends AbstractMojo {
	private static final Logger LOG = Logger.getLogger(AbstractSTJSMojo.class.getName());

	private static final Object PACKAGE_INFO_JAVA = "package-info.java";

	/**
	 * @parameter expression="${project}"
	 * @required
	 * @readonly
	 */
	protected MavenProject project;

	/**
	 * @component
	 */
	protected BuildContext buildContext;
	/**
	 * The list of packages that can be referenced from the classes that will be processed by the generator
	 * @parameter
	 */
	protected List allowedPackages;

	/**
	 * A list of inclusion filters for the compiler.
	 * @parameter
	 */
	protected Set includes = new HashSet();

	/**
	 * A list of exclusion filters for the compiler.
	 * @parameter
	 */
	protected Set excludes = new HashSet();

	/**
	 * Sets the granularity in milliseconds of the last modification date for testing whether a source needs recompilation.
	 * @parameter expression="${lastModGranularityMs}" default-value="0"
	 */
	protected int staleMillis;

	/**
	 * If true the check, if (!array.hasOwnProperty(index)) continue; is added in each "for" array iteration
	 * @parameter expression="${generateArrayHasOwnProperty}" default-value="true"
	 */
	protected boolean generateArrayHasOwnProperty;

	/**
	 * 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.
	 * @parameter expression="${generateSourceMap}" default-value="false"
	 */
	protected boolean generateSourceMap;

	/**
	 * If true, it packs all the generated Javascript file (using the correct dependency order) into a single file named
	 * ${project.artifactName}.js
	 * @parameter expression="${pack}" default-value="false"
	 */
	protected boolean pack;

	/**
	 * @parameter expression="${sourceEncoding}" default-value="${project.build.sourceEncoding}"
	 */
	private String sourceEncoding;

	/**
	 * A list of annotations to be generated
	 * @parameter
	 */
	protected Set annotations = new HashSet();

	abstract protected List getCompileSourceRoots();

	abstract protected GenerationDirectory getGeneratedSourcesDirectory() throws MojoExecutionException;

	abstract protected File getBuildOutputDirectory();

	abstract protected List getClasspathElements() throws DependencyResolutionRequiredException;

	abstract protected boolean getCopyStjsSupportFile();

	private ClassLoader getBuiltProjectClassLoader() throws MojoExecutionException {
		try {
			List runtimeClasspathElements = getClasspathElements();
			URL[] runtimeUrls = new URL[runtimeClasspathElements.size()];
			for (int i = 0; i < runtimeClasspathElements.size(); i++) {
				String element = runtimeClasspathElements.get(i);
				getLog().debug("Classpath:" + element);
				runtimeUrls[i] = new File(element).toURI().toURL();
			}
			return new URLClassLoader(runtimeUrls, Thread.currentThread().getContextClassLoader().getParent());
		}
		catch (Exception ex) {
			throw new MojoExecutionException("Cannot get builtProjectClassLoader " + ex, ex);
		}
	}

	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
		GenerationDirectory gendir = getGeneratedSourcesDirectory();

		long t1 = System.currentTimeMillis();
		getLog().info("Generating JavaScript files to " + gendir.getGeneratedSourcesAbsolutePath());

		ClassLoader builtProjectClassLoader = getBuiltProjectClassLoader();

		GeneratorConfigurationBuilder configBuilder = new GeneratorConfigurationBuilder();
		configBuilder.generateArrayHasOwnProperty(generateArrayHasOwnProperty);
		configBuilder.generateSourceMap(generateSourceMap);
		if (sourceEncoding != null) {
			configBuilder.sourceEncoding(sourceEncoding);
		}

		// configBuilder.allowedPackage("org.stjs.javascript");
		configBuilder.allowedPackage("org.junit");
		// configBuilder.allowedPackage("org.stjs.testing");

		if (allowedPackages != null) {
			configBuilder.allowedPackages(allowedPackages);
		}
		if (annotations != null) {
			configBuilder.annotations(annotations);
		}

		// scan all the packages
		for (String sourceRoot : getCompileSourceRoots()) {
			File sourceDir = new File(sourceRoot);
			Collection packages = accumulatePackages(sourceDir);
			configBuilder.allowedPackages(packages);
		}
		configBuilder.stjsClassLoader(builtProjectClassLoader);
		configBuilder.targetFolder(getBuildOutputDirectory());
		configBuilder.generationFolder(gendir);

		GeneratorConfiguration configuration = configBuilder.build();
		Generator generator = new Generator(configuration);

		int generatedFiles = 0;
		boolean hasFailures = false;
		// scan the modified sources
		for (String sourceRoot : getCompileSourceRoots()) {
			File sourceDir = new File(sourceRoot);
			SourceMapping mapping = new SuffixMapping(".java", ".js");
			SourceMapping stjsMapping = new SuffixMapping(".java", ".stjs");

			List sources = accumulateSources(gendir, sourceDir, mapping, stjsMapping, staleMillis);
			for (File source : sources) {
				if (source.getName().equals(PACKAGE_INFO_JAVA)) {
					getLog().debug("Skipping " + source);
					continue;
				}
				File absoluteSource = new File(sourceDir, source.getPath());
				try {
					File absoluteTarget =
							(File) mapping.getTargetFiles(gendir.getGeneratedSourcesAbsolutePath(), source.getPath()).iterator().next();
					if (getLog().isDebugEnabled()) {
						getLog().debug("Generating " + absoluteTarget);
					}
					buildContext.removeMessages(absoluteSource);

					if (!absoluteTarget.getParentFile().exists() && !absoluteTarget.getParentFile().mkdirs()) {
						getLog().error("Cannot create output directory:" + absoluteTarget.getParentFile());
						continue;
					}
					String className = getClassNameForSource(source.getPath());
					ClassWithJavascript stjsClass = generator.generateJavascript(className, sourceDir);
					if (!(stjsClass instanceof BridgeClass)) {
						++generatedFiles;
					}

				}
				catch (InclusionScanException e) {
					throw new MojoExecutionException("Cannot scan the source directory:" + e, e);
				}
				catch (MultipleFileGenerationException e) {
					for (JavascriptFileGenerationException jse : e.getExceptions()) {
						buildContext.addMessage(jse.getSourcePosition().getFile(), jse.getSourcePosition().getLine(),
								jse.getSourcePosition().getColumn(), jse.getMessage(), BuildContext.SEVERITY_ERROR, null);
					}
					hasFailures = true;
					// continue with the next file
				}
				catch (JavascriptFileGenerationException e) {
					buildContext.addMessage(e.getSourcePosition().getFile(), e.getSourcePosition().getLine(), e.getSourcePosition().getColumn(),
							e.getMessage(), BuildContext.SEVERITY_ERROR, null);
					hasFailures = true;
					// continue with the next file
				}
				catch (Exception e) {
					// TODO - maybe should filter more here
					buildContext.addMessage(absoluteSource, 1, 1, e.toString(), BuildContext.SEVERITY_ERROR, e);
					hasFailures = true;
					// throw new MojoExecutionException("Error generating javascript:" + e, e);
				}
			}
		}
		generator.close();
		long t2 = System.currentTimeMillis();
		getLog().info("Generated " + generatedFiles + " JavaScript files in " + (t2 - t1) + " ms");
		if (generatedFiles > 0) {
			filesGenerated(generator, gendir);
		}

		if (hasFailures) {
			throw new MojoFailureException("Errors generating JavaScript");
		}
	}

	private void detectCycles(DirectedGraph dependencyGraph) throws Exception {
		StrongConnectivityInspector inspector = new StrongConnectivityInspector(dependencyGraph);
		List> components = inspector.stronglyConnectedSets();

		for (Iterator> it = components.iterator(); it.hasNext(); ) {
			Set component = it.next();
			if (component.size() == 1) {
				it.remove();
			}
		}

		if (!components.isEmpty()) {
			throw new Exception(components.size() + " cycles are detected in the dependency graph:\n" + components.toString().replace(',', '\n')
					+ "\n Please fix the problem before continuing or disable the packing");
		}
	}

	/**
	 * packs all the files in a single file
	 * @param generator
	 * @param gendir
	 * @throws MojoFailureException
	 * @throws MojoExecutionException
	 */
	protected void packFiles(Generator generator, GenerationDirectory gendir) throws MojoFailureException, MojoExecutionException {
		if (!pack) {
			return;
		}
		OutputStream allSourcesFile = null;
		Writer packMapStream = null;
		ClassLoader builtProjectClassLoader = getBuiltProjectClassLoader();
		Map currentProjectsFiles = new HashMap();

		// pack the files
		try {
			DirectedGraph dependencyGraph = new DefaultDirectedGraph(DefaultEdge.class);
			File outputFile = new File(gendir.getGeneratedSourcesAbsolutePath(), project.getArtifactId() + ".js");
			allSourcesFile = new BufferedOutputStream(new FileOutputStream(outputFile));
			for (String sourceRoot : getCompileSourceRoots()) {
				File sourceDir = new File(sourceRoot);
				List sources = new ArrayList();
				SourceMapping mapping = new SuffixMapping(".java", ".js");
				SourceMapping stjsMapping = new SuffixMapping(".java", ".stjs");

				// take all the files
				sources = accumulateSources(gendir, sourceDir, mapping, stjsMapping, Integer.MIN_VALUE);
				for (File source : sources) {

					File absoluteTarget =
							(File) mapping.getTargetFiles(gendir.getGeneratedSourcesAbsolutePath(), source.getPath()).iterator().next();

					String className = getClassNameForSource(source.getPath());
					if (!absoluteTarget.exists()) {
						getLog().debug(className + " is a bridge. Don't add it to the pack file");
						continue;
					}
					// add this file to the hashmap to know that this class is part of the project
					currentProjectsFiles.put(className, absoluteTarget);
					if (getLog().isDebugEnabled()) {
						getLog().debug("Packing " + absoluteTarget);
					}
					ClassWithJavascript cjs =
							generator.getExistingStjsClass(builtProjectClassLoader, builtProjectClassLoader.loadClass(className));
					dependencyGraph.addVertex(className);
					for (Map.Entry dep : cjs.getDirectDependencyMap().entrySet()) {
						if (dep.getKey() instanceof STJSClass) {
							dependencyGraph.addVertex(dep.getKey().getJavaClassName());
							if (dep.getValue() != DependencyType.OTHER) {
								dependencyGraph.addEdge(dep.getKey().getJavaClassName(), className);
							}
						}
					}

				}
			}

			// check for cycles
			detectCycles(dependencyGraph);

			// dump all the files in the dependency order in the pack file
			SourceMapGeneratorV3 packSourceMap = (SourceMapGeneratorV3) SourceMapGeneratorFactory.getInstance(SourceMapFormat.V3);

			int currentLine = 0;
			Iterator it = new TopologicalOrderIterator(dependencyGraph);
			while (it.hasNext()) {
				File targetFile = currentProjectsFiles.get(it.next());
				// target file is absolute
				if (targetFile != null) {
					// for this project's files
					if (generateSourceMap) {
						currentLine = SourceMapUtils
								.appendFileSkipSourceMap(gendir.getGeneratedSourcesAbsolutePath(), allSourcesFile, targetFile, currentLine,
										packSourceMap, sourceEncoding);
					} else {
						Files.copy(targetFile, allSourcesFile);
					}
					allSourcesFile.flush();
				}
			}

			if (generateSourceMap) {
				File packMapFile = new File(gendir.getGeneratedSourcesAbsolutePath(), project.getArtifactId() + ".map");
				packMapStream = new BufferedWriter(new FileWriter(packMapFile));
				packSourceMap.appendTo(packMapStream, project.getArtifactId() + ".js");

				allSourcesFile.write(("//# sourceMappingURL=" + project.getArtifactId() + ".map\n").getBytes());
				allSourcesFile.flush();
			}

		}
		catch (Exception ex) {
			throw new MojoFailureException("Error when packing files:" + ex.getMessage(), ex);
		}
		finally {

			try {
				Closeables.close(allSourcesFile, true);
			}
			catch (IOException e) {
				LOG.log(Level.SEVERE, "IOException should not have been thrown.", e);
			}

			try {
				Closeables.close(packMapStream, true);
			}
			catch (IOException e) {
				LOG.log(Level.SEVERE, "IOException should not have been thrown.", e);
			}
		}

	}

	protected void filesGenerated(Generator generator, GenerationDirectory gendir) throws MojoFailureException, MojoExecutionException {
		// copy the javascript support
		try {
			if (getCopyStjsSupportFile()) {
				generator.copyJavascriptSupport(getGeneratedSourcesDirectory().getGeneratedSourcesAbsolutePath());
			}
		}
		catch (Exception ex) {
			throw new MojoFailureException("Error when copying support files:" + ex.getMessage(), ex);
		}

		packFiles(generator, gendir);

	}

	/**
	 * @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.
	 */
	private Collection accumulatePackages(File sourceDir) throws MojoExecutionException {
		final Collection result = new HashSet();
		if (sourceDir == null || !sourceDir.exists()) {
			return result;
		}

		DirectoryScanner ds = new DirectoryScanner();
		ds.setFollowSymlinks(true);
		ds.addDefaultExcludes();
		ds.setBasedir(sourceDir);
		ds.setIncludes(new String[] {"**/*.java"});
		ds.scan();
		for (String fileName : ds.getIncludedFiles()) {
			File file = new File(fileName);
			// Supports classes without packages
			result.add(file.getParent() == null ? "" : file.getParent().replace(File.separatorChar, '.'));
		}

		/*
		 * // 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;
	}

	private String getClassNameForSource(String sourcePath) {
		// remove ending .java and replace / by .
		return sourcePath.substring(0, sourcePath.length() - 5).replace(File.separatorChar, '.');
	}

	/**
	 * @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 accumulateSources(GenerationDirectory gendir, File sourceDir, SourceMapping jsMapping, SourceMapping stjsMapping,
			int stale) throws MojoExecutionException {
		final List 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 staleFiles = new LinkedHashSet();

		for (File f : sourceDir.listFiles()) {
			if (!f.isDirectory()) {
				continue;
			}

			try {
				staleFiles.addAll(jsScanner.getIncludedSources(f.getParentFile(), gendir.getGeneratedSourcesAbsolutePath()));
				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;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy