package com.google.gwt.maven;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import com.google.common.io.LineProcessor;
import com.google.common.io.Resources;
import com.google.gwt.dev.cfg.ModuleDef;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
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.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.classworlds.ClassWorld;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
import org.codehaus.plexus.util.xml.XMLWriter;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
import org.codehaus.plexus.util.xml.Xpp3DomWriter;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.sonatype.plexus.build.incremental.BuildContext;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Set;
/**
* Generates a GWT module definition from Maven dependencies, or merge {@code <inherits/>} with a module template.
* <p>
* When no module template exist, the behavior is identical to using an empty file.
* <p>
* {@code META-INF/gwt/mainModule} files from the project dependencies (<b>not</b> transitive) are used to generate
* {@code <inherits/>} directives. Those directives are inserted at the very beginning of the generated module
* (notably, they'll appear before any existing {@code <inherits/>} directive in the module template).
* <p>
* If {@code moduleShortName} is specified (and not empty), it <b>overwrites</b> any existing {@code rename-to} from
* the module template.
* <p>
* Unless the module template contains a source folder (either {@code <source/>} or {@code <super-source/>}, those
* three lines will be inserted at the very end of the generated module (this is to keep any {@code includes} or
* {@code excludes} or specific {@code path} from the module template):
* <pre><code>
* <source path="client"/>
* <source path="shared"/>
* <super-source path="super"/>
* </code></pre>
*/
@Mojo(name = "generate-module", threadSafe = true, defaultPhase = LifecyclePhase.GENERATE_RESOURCES, requiresDependencyResolution = ResolutionScope.RUNTIME)
public class GenerateModuleMojo extends AbstractMojo {
/**
* The directory where the GWT module descriptor will be generated.
*/
@Parameter(defaultValue = "${project.build.outputDirectory}")
private File outputDirectory;
/**
* The module to generate.
*/
@Parameter
private String moduleName;
/**
* The short name of the module, used to name the output {@code .nocache.js} file.
*/
@Parameter
private String moduleShortName;
/**
* Module definition to merge with.
*/
@Parameter(defaultValue = "${project.basedir}/src/main/module.gwt.xml")
private File moduleTemplate;
/**
* A flag to disable generation of the GWT module in favor of a hand-authored module descriptor.
*/
@Parameter(defaultValue = "false")
private boolean skipModule;
@Parameter(defaultValue = "${project.dependencyArtifacts}", required = true, readonly = true)
private Set<Artifact> dependencyArtifacts;
@Component
private BuildContext buildContext;
@Parameter(defaultValue = "${project}", required = true, readonly = true)
private MavenProject project;
private final ScopeArtifactFilter artifactFilter = new ScopeArtifactFilter(Artifact.SCOPE_COMPILE_PLUS_RUNTIME);
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (skipModule) {
return;
}
if (StringUtils.isBlank(moduleName)) {
throw new MojoExecutionException("Missing moduleName");
}
if (!ModuleDef.isValidModuleName(moduleName)) {
throw new MojoExecutionException("Invalid module name: " + moduleName);
}
File outputFile = new File(outputDirectory, moduleName.replace('.', '/') + ".gwt.xml");
boolean uptodate;
Xpp3Dom template;
if (moduleTemplate.isFile()) {
uptodate = buildContext.isUptodate(outputFile, moduleTemplate);
try {
template = Xpp3DomBuilder.build(Files.newReader(moduleTemplate, Charsets.UTF_8));
} catch (XmlPullParserException e) {
throw new MojoExecutionException(e.getMessage(), e);
} catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
} else {
uptodate = true;
template = new Xpp3Dom("module");
}
if (uptodate) {
uptodate = buildContext.isUptodate(outputFile, project.getFile());
}
if (uptodate) {
// TODO: check dependencies (META-INF/gwt/mainModule might have changed)
// For now, we'll rely on newFileOutputStream's internal staleness-check behavior
getLog().info("Module is up to date; skipping.");
}
outputFile.getParentFile().mkdirs();
Writer writer = null;
try {
writer = new OutputStreamWriter(buildContext.newFileOutputStream(outputFile), Charsets.UTF_8);
XMLWriter xmlWriter = new PrettyPrintXMLWriter(writer);
xmlWriter.startElement("module");
// override or copy rename-to
String oldRenameTo = template.getAttribute("rename-to");
if (!StringUtils.isBlank(moduleShortName)) {
if (oldRenameTo != null) {
getLog().info("Overriding module short name " + oldRenameTo + " with " + moduleShortName);
}
xmlWriter.addAttribute("rename-to", moduleShortName);
} else if (oldRenameTo != null) {
xmlWriter.addAttribute("rename-to", oldRenameTo);
}
// copy other attributes
for (String attrName : template.getAttributeNames()) {
if ("rename-to".equals(attrName)) {
continue;
}
xmlWriter.addAttribute(attrName, template.getAttribute(attrName));
}
boolean hasInherits = generateInheritsFromDependencies(xmlWriter);
// copy children
boolean hasSource = false;
for (Xpp3Dom child : template.getChildren()) {
if ("inherits".equals(child.getName())) {
hasInherits = true;
} else if ("source".equals(child.getName()) || "super-source".equals(child.getName())) {
hasSource = true;
}
Xpp3DomWriter.write(xmlWriter, child);
}
// insert <inherits name="com.google.gwt.core.Core"/> if no other inherited module
if (!hasInherits) {
xmlWriter.startElement("inherits");
xmlWriter.addAttribute("name", "com.google.gwt.core.Core");
xmlWriter.endElement();
}
if (!hasSource) {
// <source path="client" />
xmlWriter.startElement("source");
xmlWriter.addAttribute("path", "client");
xmlWriter.endElement();
// <source path="shared" />
xmlWriter.startElement("source");
xmlWriter.addAttribute("path", "shared");
xmlWriter.endElement();
// <super-source path="super" />
xmlWriter.startElement("super-source");
xmlWriter.addAttribute("path", "super");
xmlWriter.endElement();
}
xmlWriter.endElement(); // module
} catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
} finally {
IOUtil.close(writer);
}
}
private boolean generateInheritsFromDependencies(XMLWriter xmlWriter) throws MojoExecutionException {
ClassWorld world = new ClassWorld();
ClassRealm realm;
try {
realm = world.newRealm("gwt", null);
for (Artifact artifact : dependencyArtifacts) {
if (!artifactFilter.include(artifact)) {
continue;
}
if (!artifact.getArtifactHandler().isAddedToClasspath()) {
continue;
}
realm.addURL(artifact.getFile().toURI().toURL());
}
} catch (DuplicateRealmException e) {
throw new MojoExecutionException(e.getMessage(), e);
} catch (MalformedURLException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
boolean hasInherits = false;
try {
Enumeration<URL> resources = realm.getResources("META-INF/gwt/mainModule");
while (resources.hasMoreElements()) {
final URL resource = resources.nextElement();
String moduleName = Resources.readLines(resource, Charsets.UTF_8, new LineProcessor<String>() {
private String module;
@Override
public boolean processLine(String line) throws IOException {
line = StringUtils.substringBefore(line, "#").trim();
if (line.isEmpty()) {
return true;
}
if (module != null) {
getLog().warn("Configuration file contains more than one module name, picking first: " + resource);
return false;
}
if (!ModuleDef.isValidModuleName(line)) {
getLog().warn("Illegal configuration-file syntax, skipping " + resource);
return false;
}
module = line;
// Continue processing lines to warn of illegal syntax
return true;
}
@Override
public String getResult() {
return module;
}
});
xmlWriter.startElement("inherits");
xmlWriter.addAttribute("name", moduleName);
xmlWriter.endElement();
}
} catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
return hasInherits;
}
}