package org.appfuse.mojo.installer;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.io.FileUtils;
import org.apache.maven.embedder.MavenEmbedder;
import org.apache.maven.embedder.MavenEmbedderConsoleLogger;
import org.apache.maven.model.Dependency;
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.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Get;
import org.apache.tools.ant.taskdefs.LoadFile;
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp;
import org.appfuse.tool.SubversionUtils;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import static java.util.Arrays.asList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/**
* This mojo is used to "install" source artifacts from Subversion into an AppFuse project.
*
* @author Matt Raible
* @goal full-source
*/
public class InstallSourceMojo extends AbstractMojo {
private static final String APPFUSE_GROUP_ID = "org.appfuse";
private static final List JSF_PROPERTIES = asList("ajax4jsf", "myfaces.tomahawk", "corejsf.validator", "myfaces", "facelets", "el");
private static final List SPRING_PROPERTIES = asList("springmodules.validation");
private static final List STRUTS_PROPERTIES = asList("struts");
private static final List TAPESTRY_PROPERTIES = asList("tapestry.flash", "tapestry", "tapestry.spring");
private String daoFramework;
private String webFramework;
Project antProject = AntUtils.createProject();
/**
* The path where the files from SVN will be placed. This is intentionally set to "src" since that's the default
* src directory used for exporting AppFuse artifacts.
*
* @parameter expression="${appfuse.destinationDirectory}" default-value="${basedir}/src"
*/
private String destinationDirectory;
/**
* The directory containing the source code.
*
* @parameter expression="${appfuse.trunk}" default-value="https://appfuse.dev.java.net/svn/appfuse/"
*/
private String trunk;
/**
* The tag containing the source code - defaults to '/trunk', but you may want to set it to '/tags/TAGNAME'
*
* @parameter expression="${appfuse.tag}" default-value="trunk/"
*/
private String tag;
/**
* Maven Internal: Project to interact with.
*
* @parameter expression="${project}"
* @required
* @readonly
* @noinspection UnusedDeclaration
*/
private MavenProject project;
public void execute() throws MojoExecutionException, MojoFailureException {
// todo: get this working from the top-level directory - only works with basic project for now
if (!project.getPackaging().equalsIgnoreCase("war")) {
String errorMsg = "This plugin can currently only be run from an AppFuse web project (packaging == 'war').";
//getLog().error(errorMsg);
throw new MojoFailureException(errorMsg);
}
// If appfuse.version is specified as a property, and not a SNAPSHOT, use it for the tag
String appfuseVersion = project.getProperties().getProperty("appfuse.version");
if (appfuseVersion != null && !appfuseVersion.endsWith("SNAPSHOT") && tag.equals("trunk/")) {
// todo: convert version to match tag
}
daoFramework = project.getProperties().getProperty("dao.framework");
webFramework = project.getProperties().getProperty("web.framework");
// install dao and manager source if modular/core or war writer/o parent (basic)
if (project.getPackaging().equals("jar") || (project.getPackaging().equals("war") && project.getParent() == null)) {
log("Installing source from data modules...");
// export data-common
export("data/common/src");
// export persistence framework
export("data/" + getDaoFramework() + "/src");
// export service module
log("Installing source from service module...");
export("service/src");
// add dependencies from appfuse-service to pom.xml
}
if (project.getPackaging().equalsIgnoreCase("war")) {
// export web-common
log("Installing source from web-common module...");
export("web/common/src");
// export web framework
log("Installing source from " + webFramework + " module...");
export("web/" + webFramework + "/src");
}
log("Source successfully exported, modifying pom.xml...");
removeWarpathPlugin(new File("pom.xml"));
List dependencies = project.getOriginalModel().getDependencies();
List newDependencies = new ArrayList();
// remove all appfuse dependencies
for (Object dependency : dependencies) {
Dependency dep = (Dependency) dependency;
if (!dep.getGroupId().equals(APPFUSE_GROUP_ID)) {
newDependencies.add(dep);
}
}
// add dependencies from root appfuse pom
newDependencies = addModuleDependencies(newDependencies, "root", "");
// Add dependencies from appfuse-${dao.framework}
newDependencies = addModuleDependencies(newDependencies, daoFramework, "data/" + daoFramework);
// Add dependencies from appfuse-service
newDependencies = addModuleDependencies(newDependencies, "service", "service");
// Add dependencies from appfuse-common-web
newDependencies = addModuleDependencies(newDependencies, "web-common", "web/common");
// Add dependencies from appfuse-${web.framework}
newDependencies = addModuleDependencies(newDependencies, webFramework, "web/" + webFramework, true);
// Change spring-mock and jmock dependencies to use true instead of test.
// This is necessary because Base*TestCase classes are in src/main/java. If we move these classes to their
// own test module, this will no longer be necessary. For the first version of this mojo, it seems easier
// to follow the convention used in AppFuse rather than creating a test module and changing all modules to
// depend on it.
for (Dependency dep : newDependencies) {
if (dep.getArtifactId().equals("spring-mock") || dep.getArtifactId().equals("jmock") || dep.getArtifactId().equals("junit")) {
dep.setOptional(true);
dep.setScope(null);
}
}
Collections.sort(newDependencies, new BeanComparator("groupId"));
project.getOriginalModel().setDependencies(newDependencies);
// todo: figure out how to get these written in alphabetical order
project.getOriginalModel().setProperties(getAppFuseProperties());
StringWriter writer = new StringWriter();
try {
project.writeOriginalModel(writer);
File pom = new File("pom-fullsource.xml");
if (pom.exists()) {
pom.delete();
}
FileWriter fw = new FileWriter(pom);
fw.write(writer.toString());
fw.flush();
fw.close();
} catch (IOException ex) {
getLog().error("Unable to create pom-fullsource.xml: " + ex.getMessage(), ex);
throw new MojoFailureException(ex.getMessage());
}
log("Updated dependencies in pom.xml...");
// I tried to use regex here, but couldn't get it to work - going with the old fashioned way instead
String pomXml = writer.toString();
int startTag = pomXml.indexOf("\n ");
String dependencyXml = pomXml.substring(startTag, pomXml.indexOf("", startTag));
// change 2 spaces to 4
dependencyXml = dependencyXml.replaceAll(" ", " ");
dependencyXml = " " + dependencyXml;
try {
String originalPom = FileUtils.readFileToString(new File("pom.xml"));
startTag = originalPom.indexOf("\n ");
StringBuffer sb = new StringBuffer();
sb.append(originalPom.substring(0, startTag));
sb.append(dependencyXml);
sb.append(originalPom.substring(originalPom.indexOf("", startTag)));
startTag = pomXml.indexOf("\n ");
String propertiesXml = pomXml.substring(startTag + 16, pomXml.lastIndexOf("\n "));
// change 2 spaces to 4
propertiesXml = propertiesXml.replaceAll(" ", " ");
// add the Spring version to the new properties since spring.version is replaced below
propertiesXml = " " + project.getProperties().getProperty("spring.version") + "\n" + propertiesXml;
propertiesXml = " \n" + propertiesXml;
// add new properties
String pomWithProperties = sb.toString().replaceFirst(" (.*)", propertiesXml);
FileUtils.writeStringToFile(new File("pom.xml"), pomWithProperties);
} catch (IOException ex) {
getLog().error("Unable to write to pom.xml: " + ex.getMessage(), ex);
throw new MojoFailureException(ex.getMessage());
}
log("Yeehaw - it worked! Unfortunately, this plugin doesn't rename packages yet, but it will in 2.0 Final");
log("If you manually rename your packages, make sure and set to true.");
// todo: rename packages
// todo: gather and add repositories from appfuse projects
// should work for now since most artifacts are in static.appfuse.org/repository
// cleanup so user isn't aware that files were created
File pom = new File("pom-fullsource.xml");
if (pom.exists()) {
pom.delete();
}
}
private void export(String url) throws MojoExecutionException {
SubversionUtils svn = new SubversionUtils(trunk + tag + url, destinationDirectory);
try {
svn.export();
} catch (SVNException e) {
SVNErrorMessage err = e.getErrorMessage();
/*
* Display all tree of error messages.
* Utility method SVNErrorMessage.getFullMessage() may be used instead of the loop.
*/
while (err != null) {
getLog().error(err.getErrorCode().getCode() + " : " + err.getMessage());
err = err.getChildErrorMessage();
}
throw new MojoExecutionException(e.getMessage());
}
}
private String getDaoFramework() {
String fw = project.getProperties().getProperty("dao.framework");
if (fw.equalsIgnoreCase("jpa-hibernate")) {
return "jpa";
} else {
return fw;
}
}
private void log(String msg) {
getLog().info("[AppFuse] " + msg);
}
private void removeWarpathPlugin(File pom) {
log("Removing maven-warpath-plugin...");
Project antProject = AntUtils.createProject();
ReplaceRegExp regExpTask = (ReplaceRegExp) antProject.createTask("replaceregexp");
regExpTask.setFile(pom);
regExpTask.setMatch("\\s*\\s*org.appfuse(?s:.)*?maven-warpath-plugin(?s:.)*?");
regExpTask.setReplace("");
regExpTask.setFlags("g");
regExpTask.execute();
// remove any warpath dependencies as well
ReplaceRegExp regExpTask2 = (ReplaceRegExp) antProject.createTask("replaceregexp");
regExpTask2.setFile(pom);
regExpTask2.setMatch("\\s*\\s*\\$\\{pom\\.groupId\\}(?s:.)*?warpath(?s:.)*?");
regExpTask2.setReplace("");
regExpTask2.setFlags("g");
regExpTask2.execute();
ReplaceRegExp regExpTask3 = (ReplaceRegExp) antProject.createTask("replaceregexp");
regExpTask3.setFile(pom);
regExpTask3.setMatch("\\s*\\s*org.appfuse(?s:.)*?warpath(?s:.)*?");
regExpTask3.setReplace("");
regExpTask3.setFlags("g");
regExpTask3.execute();
}
// Convenience method that doesn't remove warpath plugin
private List addModuleDependencies(List dependencies, String moduleName, String moduleLocation) {
return addModuleDependencies(dependencies, moduleName, moduleLocation, false);
}
private List addModuleDependencies(List dependencies, String moduleName, String moduleLocation, boolean removeWarpath) {
log("Adding dependencies from '" + moduleName + "' module...");
// Read dependencies from module's pom.xml
URL pomLocation = null;
File newDir = new File("target", "appfuse-" + moduleName);
if (!newDir.exists()) {
newDir.mkdirs();
}
File pom = new File("target/appfuse-" + moduleName + "/pom.xml");
try {
pomLocation = new URL(trunk + tag + moduleLocation + "/pom.xml");
} catch (MalformedURLException e) {
e.printStackTrace();
}
Get get = (Get) AntUtils.createProject().createTask("get");
get.setSrc(pomLocation);
get.setDest(pom);
get.setUsername("guest");
get.setPassword("");
get.execute();
if (removeWarpath) {
this.removeWarpathPlugin(pom);
}
MavenEmbedder maven = new MavenEmbedder();
maven.setOffline(true);
maven.setClassLoader(Thread.currentThread().getContextClassLoader());
maven.setLogger(new MavenEmbedderConsoleLogger());
MavenProject p = null;
try {
maven.start();
p = maven.readProjectWithDependencies(pom);
maven.stop();
} catch (Exception e) {
e.printStackTrace();
}
List moduleDependencies = p.getOriginalModel().getDependencies();
// create a list of artifactIds to check for duplicates (there's no equals() on Dependency)
Set artifactIds = new LinkedHashSet();
for (Dependency dep : dependencies) {
artifactIds.add(dep.getArtifactId());
}
// add all non-appfuse dependencies
for (Object moduleDependency : moduleDependencies) {
Dependency dep = (Dependency) moduleDependency;
if (!artifactIds.contains(dep.getArtifactId()) && !dep.getArtifactId().contains("appfuse")) {
dependencies.add(dep);
}
}
return dependencies;
}
private Properties getAppFuseProperties() {
log("Getting properties from 'appfuse' module...");
// Read dependencies from module's pom.xml
URL pomLocation = null;
File newDir = new File("target", "appfuse");
if (!newDir.exists()) {
newDir.mkdirs();
}
File pom = new File("target/appfuse/pom.xml");
try {
pomLocation = new URL(trunk + tag + "/pom.xml");
} catch (MalformedURLException e) {
e.printStackTrace();
}
Get get = (Get) AntUtils.createProject().createTask("get");
get.setSrc(pomLocation);
get.setDest(pom);
get.setUsername("guest");
get.setPassword("");
get.execute();
MavenEmbedder maven = new MavenEmbedder();
maven.setOffline(true);
maven.setClassLoader(Thread.currentThread().getContextClassLoader());
maven.setLogger(new MavenEmbedderConsoleLogger());
MavenProject p = null;
try {
maven.start();
p = maven.readProjectWithDependencies(pom);
maven.stop();
} catch (Exception e) {
e.printStackTrace();
}
Properties props = p.getOriginalModel().getProperties();
Properties projectProperties = project.getOriginalModel().getProperties();
Properties newProperties = new Properties();
// don't include dao and web framework dependencies
List webDependencies = new ArrayList();
webDependencies.addAll(JSF_PROPERTIES);
webDependencies.addAll(SPRING_PROPERTIES);
webDependencies.addAll(STRUTS_PROPERTIES);
webDependencies.addAll(TAPESTRY_PROPERTIES);
for (Object o : props.keySet()) {
String key = (String) o;
if (!projectProperties.containsKey(key)) {
newProperties.put(key, props.getProperty(key));
}
}
// NOTE: There's *got* to be a cleaner way to do this, but I can't think of it right now
// ensure properties are only for the current frameworks being used
// PROPOSED FIX: Read versions from webapp pom.xml and link keys to properties
for (String key : webDependencies) {
newProperties.remove(key + ".version");
}
newProperties.remove("hibernate.version");
newProperties.remove("ibatis.version");
newProperties.remove("jpa.version");
if (webFramework.equalsIgnoreCase("jsf")) {
for (String key : JSF_PROPERTIES) {
key += ".version";
newProperties.put(key, props.getProperty(key));
}
} else if (webFramework.equalsIgnoreCase("spring")) {
for (String key : SPRING_PROPERTIES) {
key += ".version";
newProperties.put(key, props.getProperty(key));
}
} else if (webFramework.equalsIgnoreCase("struts")) {
for (String key : STRUTS_PROPERTIES) {
key += ".version";
newProperties.put(key, props.getProperty(key));
}
} else if (webFramework.equalsIgnoreCase("tapestry")) {
for (String key : TAPESTRY_PROPERTIES) {
key += ".version";
newProperties.put(key, props.getProperty(key));
}
}
newProperties.put(getDaoFramework() + ".version", props.getProperty(getDaoFramework() + ".version"));
return newProperties;
}
/**
* This method will create an ANT based LoadFile task based on an infile and a property name.
* The property will be loaded with the infile for use later by the Replace task.
*
* @param inFile The file to process
* @param propName the name to assign it to
* @return The ANT LoadFile task that loads a property with a file
*/
protected LoadFile createLoadFileTask(String inFile, String propName) {
LoadFile loadFileTask = (LoadFile) antProject.createTask("loadfile");
loadFileTask.init();
loadFileTask.setProperty(propName);
loadFileTask.setSrcFile(new File(inFile));
return loadFileTask;
}
}