/*
* Copyright 2012 James Moger
*
* 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.moxie.ant;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.InitCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.StoredConfig;
import org.moxie.MoxieException;
import org.moxie.Scope;
import org.moxie.Toolkit;
import org.moxie.Toolkit.Key;
import org.moxie.maxml.Maxml;
import org.moxie.maxml.MaxmlException;
import org.moxie.maxml.MaxmlMap;
import org.moxie.utils.FileUtils;
import org.moxie.utils.StringUtils;
public class Main extends org.apache.tools.ant.Main implements BuildListener {
NewProject newProject = null;
/**
* Command line entry point. This method kicks off the building
* of a project object and executes a build using either a given
* target or the default target.
*
* @param args Command line arguments. Must not be <code>null</code>.
*/
public static void main(String[] args) {
start(args, null, null);
}
/**
* Creates a new instance of this class using the
* arguments specified, gives it any extra user properties which have been
* specified, and then runs the build using the classloader provided.
*
* @param args Command line arguments. Must not be <code>null</code>.
* @param additionalUserProperties Any extra properties to use in this
* build. May be <code>null</code>, which is the equivalent to
* passing in an empty set of properties.
* @param coreLoader Classloader used for core classes. May be
* <code>null</code> in which case the system classloader is used.
*/
public static void start(String[] args, Properties additionalUserProperties,
ClassLoader coreLoader) {
Main m = new Main();
m.startAnt(args, additionalUserProperties, coreLoader);
}
/*
* (non-Javadoc)
*
* @see org.apache.tools.ant.Main#startAnt(java.lang.String[],
* java.util.Properties, java.lang.ClassLoader)
*/
@Override
public void startAnt(String[] args, Properties additionalUserProperties, ClassLoader coreLoader) {
boolean startAnt = true;
boolean specifiedBuildFile = false;
List<String> antArgs = new ArrayList<String>(Arrays.asList(args));
if (antArgs.contains("-color")) {
antArgs.remove("-color");
System.setProperty(Toolkit.MX_COLOR, "true");
}
if (antArgs.contains("-c")) {
antArgs.remove("-c");
System.setProperty(Toolkit.MX_COLOR, "true");
}
if (antArgs.contains("-debug") || antArgs.contains("-d")) {
System.setProperty(Toolkit.MX_DEBUG, "true");
}
if (antArgs.contains("-verbose") || antArgs.contains("-v")) {
System.setProperty(Toolkit.MX_VERBOSE, "true");
}
if (antArgs.contains("-updateMetadata")) {
antArgs.remove("-updateMetadata");
System.setProperty(Toolkit.MX_UPDATEMETADATA, "true");
}
if (antArgs.contains("-ignoreChecksums")) {
antArgs.remove("-ignoreChecksums");
System.setProperty(Toolkit.MX_ENFORCECHECKSUMS, "false");
}
if (antArgs.contains("-offline")) {
antArgs.remove("-offline");
System.setProperty(Toolkit.MX_ONLINE, "false");
}
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (arg.equals("-help") || arg.equals("-h")) {
printUsage();
startAnt = false;
} else if (arg.equals("-version")) {
printVersion();
startAnt = false;
} else if (arg.equals("-new")) {
// create new project
antArgs.remove(arg);
newProject = newProject(antArgs);
// run the init phase on the new project
antArgs = Arrays.asList(
"phase:init",
"-Dbasedir=" + newProject.dir.getAbsolutePath().replace('\\', '/'),
"-f",
new File(newProject.dir, "build.xml").getAbsolutePath().replace('\\', '/'));
break;
} else if (arg.equals("-f") || arg.equals("-buildfile") || arg.equals("-file")) {
specifiedBuildFile = true;
}
}
if (startAnt) {
List<String> moxieArgs = new ArrayList<String>();
moxieArgs.add("-logger");
moxieArgs.add(MainLogger.class.getName());
if (!specifiedBuildFile) {
File file = new File(System.getProperty("user.dir"), "build.xml");
if (file.exists()) {
// standard local build.xml
moxieArgs.add("-f");
moxieArgs.add("build.xml");
} else {
// missing build file, use minimal file
try {
InputStream is = getClass().getResourceAsStream("/archetypes/build.xml");
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte [] buffer = new byte[4096];
int len = 0;
while((len = is.read(buffer)) > -1) {
os.write(buffer, 0, len);
}
String content = os.toString("UTF-8");
os.close();
is.close();
file = File.createTempFile("build-", ".xml", new File(System.getProperty("user.dir")));
file.deleteOnExit();
FileUtils.writeContent(file, content);
moxieArgs.add("-f");
moxieArgs.add(file.getAbsolutePath());
} catch (Throwable t) {
t.printStackTrace();
System.exit(1);
}
}
}
moxieArgs.addAll(antArgs);
String [] newArgs = moxieArgs.toArray(new String[moxieArgs.size()]);
super.startAnt(newArgs, additionalUserProperties, coreLoader);
}
}
@Override
protected void addBuildListeners(Project project) {
project.addBuildListener(this);
super.addBuildListeners(project);
}
private void printVersion() {
System.out.println();
System.out.println("Moxie+Ant v" + Toolkit.getVersion());
System.out.println("executing on " + getAntVersion());
System.out.println();
}
/**
* Prints the usage information for this class to <code>System.out</code>.
*/
private void printUsage() {
String lSep = System.getProperty("line.separator");
StringBuilder msg = new StringBuilder();
msg.append("moxie [options] [target [target2 [target3] ...]]" + lSep);
msg.append("Options: " + lSep);
msg.append(" -help, -h print this message" + lSep);
msg.append(" -projecthelp, -p print project help information" + lSep);
msg.append(" -version print the version information and exit" + lSep);
msg.append(" -diagnostics print information that might be helpful to" + lSep);
msg.append(" diagnose or report problems." + lSep);
msg.append(lSep);
msg.append(" -new -<archetype> <groupId>:<artifactId>:<version> -dir:<dirname> -git<:originId> -eclipse<:+var> -intellij" + lSep);
msg.append(lSep);
msg.append(" -offline do not contact any remote repositories" + lSep);
msg.append(" -ignoreChecksums disable SHA1 checksum verification" + lSep);
msg.append(" -updateMetadata force metadata updates" + lSep);
msg.append(" -color, -c use ANSI color sequences" + lSep);
msg.append(" -quiet, -q be extra quiet" + lSep);
msg.append(" -verbose, -v be extra verbose" + lSep);
msg.append(" -debug, -d print debugging information" + lSep);
msg.append(" -emacs, -e produce logging information without adornments"
+ lSep);
msg.append(" -lib <path> specifies a path to search for jars and classes"
+ lSep);
msg.append(" -logfile <file> use given file for log" + lSep);
msg.append(" -l <file> ''" + lSep);
msg.append(" -logger <classname> the class which is to perform logging" + lSep);
msg.append(" -listener <classname> add an instance of class as a project listener"
+ lSep);
msg.append(" -noinput do not allow interactive input" + lSep);
msg.append(" -buildfile <file> use given buildfile" + lSep);
msg.append(" -file <file> ''" + lSep);
msg.append(" -f <file> ''" + lSep);
msg.append(" -D<property>=<value> use value for given property" + lSep);
msg.append(" -keep-going, -k execute all targets that do not depend" + lSep);
msg.append(" on failed target(s)" + lSep);
msg.append(" -propertyfile <name> load all properties from file with -D" + lSep);
msg.append(" properties taking precedence" + lSep);
msg.append(" -inputhandler <class> the class which will handle input requests" + lSep);
msg.append(" -find <file> (s)earch for buildfile towards the root of" + lSep);
msg.append(" -s <file> the filesystem and use it" + lSep);
msg.append(" -nice number A niceness value for the main thread:" + lSep
+ " 1 (lowest) to 10 (highest); 5 is the default"
+ lSep);
msg.append(" -nouserlib Run Moxie without using the jar files from" + lSep
+ " ${user.home}/.ant/lib" + lSep);
msg.append(" -noclasspath Run Moxie without using CLASSPATH" + lSep);
msg.append(" -autoproxy Java1.5+: use the OS proxy settings"
+ lSep);
msg.append(" -main <class> override Moxie's normal entry point");
System.out.println(msg.toString());
}
/**
* Creates a new Moxie project in the current folder.
*
* @param args
*/
private NewProject newProject(List<String> args) {
File basedir = new File(System.getProperty("user.dir"));
File moxieFile = new File(basedir, "build.moxie");
if (moxieFile.exists()) {
log("build.moxie exists! Can not create new project!");
System.exit(1);
}
NewProject project = new NewProject();
project.dir = basedir;
List<String> apply = new ArrayList<String>();
// parse args
if (args.size() > 0) {
List<String> projectArgs = new ArrayList<String>();
for (String arg : args) {
if (arg.startsWith("-git")) {
project.initGit = true;
if (arg.startsWith("-git:")) {
project.gitOrigin = arg.substring(5);
}
} else if (arg.startsWith("-eclipse")) {
if (args.contains("+var")) {
// Eclipse uses variable-relative paths
project.eclipse = Eclipse.var;
} else if (args.contains("+ext")) {
// Eclipse uses project-relative paths
project.eclipse = Eclipse.ext;
} else {
// Eclipse uses hard-coded paths to MOXIE_HOME in USER_HOME
project.eclipse = Eclipse.user;
}
apply.add(arg.substring(1));
} else if (arg.startsWith("-intellij")) {
project.idea = IntelliJ.var;
apply.add(arg.substring(1));
} else if (arg.startsWith("-apply:")) {
// -apply:a,b,c,d
// -apply:a -apply:b
List<String> vals = new ArrayList<String>();
for (String val : arg.substring(arg.indexOf(':') + 1).split(",")) {
vals.add(val.trim());
}
apply.addAll(vals);
// special apply cases
for (String val : vals) {
if (val.startsWith(Toolkit.APPLY_ECLIPSE)) {
if (args.contains("+var")) {
// Eclipse uses variable-relative paths
project.eclipse = Eclipse.var;
} else if (args.contains("+ext")) {
// Eclipse uses project-relative paths
project.eclipse = Eclipse.ext;
} else {
// Eclipse uses hard-coded paths to MOXIE_HOME in USER_HOME
project.eclipse = Eclipse.user;
}
} else if (val.equals(Toolkit.APPLY_INTELLIJ)) {
project.idea = IntelliJ.var;
}
}
} else if (arg.startsWith("-dir:")) {
String dir = arg.substring("-dir:".length()).trim();
project.dir = new File(basedir, dir);
} else {
projectArgs.add(arg);
}
}
// parse
project.type = projectArgs.get(0);
if (project.type.charAt(0) == '-') {
project.type = project.type.substring(1);
}
if (projectArgs.size() > 1) {
String [] fields = projectArgs.get(1).split(":");
switch (fields.length) {
case 2:
project.groupId = fields[0];
project.artifactId = fields[1];
break;
case 3:
project.groupId = fields[0];
project.artifactId = fields[1];
project.version = fields[2];
break;
default:
throw new MoxieException("Illegal parameter " + args);
}
}
}
InputStream is = getClass().getResourceAsStream(MessageFormat.format("/archetypes/{0}.moxie", project.type));
if (is == null) {
log("Unknown archetype " + project.type);
System.exit(1);
}
MaxmlMap map = null;
try {
map = Maxml.parse(is);
} catch (MaxmlException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (Exception e) {
}
}
// property substitution
Map<String, String> properties = new HashMap<String, String>();
properties.put(Key.groupId.name(), project.groupId);
properties.put(Key.artifactId.name(), project.artifactId);
properties.put(Key.version.name(), project.version);
properties.put(Key.apply.name(), StringUtils.flattenStrings(apply, ", "));
for (String key : map.keySet()) {
Object o = map.get(key);
if (o instanceof String) {
String value = resolveProperties(o.toString(), properties);
map.put(key, value);
}
}
createDirectories(project, map, Key.sourceDirectories);
createDirectories(project, map, Key.resourceDirectories);
// Eclipse-ext dependency directory
if (Eclipse.ext.equals(project.eclipse)) {
map.put(Toolkit.Key.dependencyDirectory.name(), "ext");
}
// write build.moxie
String maxml = map.toMaxml();
moxieFile = new File(project.dir, "build.moxie");
FileUtils.writeContent(moxieFile, maxml);
// write build.xml
try {
is = getClass().getResourceAsStream(MessageFormat.format("/archetypes/{0}.build.xml", project.type));
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte [] buffer = new byte[4096];
int len = 0;
while((len = is.read(buffer)) > -1) {
os.write(buffer, 0, len);
}
String prototype = os.toString("UTF-8");
os.close();
is.close();
String content = prototype;
content = content.replace("%MOXIE_VERSION%", Toolkit.getVersion());
content = content.replace("%MOXIE_URL%", Toolkit.getMavenUrl());
FileUtils.writeContent(new File(project.dir, "build.xml"), content);
} catch (Throwable t) {
t.printStackTrace();
System.exit(1);
}
return project;
}
private void createDirectories(NewProject project, MaxmlMap map, Key key) {
List<String> dirs = map.getStrings(key.name(), Arrays.asList(new String [0]));
for (String scopedDir : dirs) {
String s = scopedDir.substring(0, scopedDir.indexOf(' ')).trim();
Scope scope = Scope.fromString(s);
if (scope.isValidSourceScope()) {
String folder = StringUtils.stripQuotes(scopedDir.substring(s.length() + 1).trim());
File file = new File(project.dir, folder);
file.mkdirs();
} else {
log("Illegal " + key.name() + " scope: " + s);
}
}
}
private void initGit() throws GitAPIException {
// create the repository
InitCommand init = Git.init();
init.setBare(false);
init.setDirectory(newProject.dir);
Git git = init.call();
if (!StringUtils.isEmpty(newProject.gitOrigin)) {
// set the origin and configure the master branch for merging
StoredConfig config = git.getRepository().getConfig();
config.setString("remote", "origin", "url", getGitUrl());
config.setString("remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*");
config.setString("branch", "master", "remote", "origin");
config.setString("branch", "master", "merge", "refs/heads/master");
try {
config.save();
} catch (IOException e) {
throw new MoxieException(e);
}
}
// prepare a common gitignore file
StringBuilder sb = new StringBuilder();
sb.append("/.directory\n");
sb.append("/.DS_STORE\n");
sb.append("/.DS_Store\n");
sb.append("/.settings\n");
sb.append("/bin\n");
sb.append("/build\n");
sb.append("/ext\n");
sb.append("/target\n");
sb.append("/tmp\n");
sb.append("/temp\n");
if (!newProject.eclipse.includeClasspath()) {
// ignore hard-coded .classpath
sb.append("/.classpath\n");
}
FileUtils.writeContent(new File(newProject.dir, ".gitignore"), sb.toString());
AddCommand add = git.add();
add.addFilepattern("build.xml");
add.addFilepattern("build.moxie");
add.addFilepattern(".gitignore");
if (newProject.eclipse.includeProject()) {
add.addFilepattern(".project");
}
if (newProject.eclipse.includeClasspath()) {
// MOXIE_HOME relative dependencies in .classpath
add.addFilepattern(".classpath");
}
if (newProject.idea.includeProject()) {
add.addFilepattern(".project");
}
if (newProject.idea.includeClasspath()) {
// MOXIE_HOME relative dependencies in .iml
add.addFilepattern("*.iml");
}
try {
add.call();
CommitCommand commit = git.commit();
PersonIdent moxie = new PersonIdent("Moxie", "moxie@localhost");
commit.setAuthor(moxie);
commit.setCommitter(moxie);
commit.setMessage("Project structure created");
commit.call();
} catch (Exception e) {
throw new MoxieException(e);
}
}
String resolveProperties(String string, Map<String, String> properties) {
if (string == null) {
return null;
}
Pattern p = Pattern.compile("\\$\\{[a-zA-Z0-9-_\\.]+\\}");
StringBuilder sb = new StringBuilder(string);
while (true) {
Matcher m = p.matcher(sb.toString());
if (m.find()) {
String prop = m.group();
prop = prop.substring(2, prop.length() - 1);
String value = prop;
if (properties.containsKey(prop)) {
value = properties.get(prop);
}
sb.replace(m.start(), m.end(), value);
} else {
return sb.toString();
}
}
}
/**
* Returns a git url from the specified url which may use aliases.
*
* @return a url
*/
private String getGitUrl() {
String url = newProject.gitOrigin;
String repo = newProject.gitOrigin.substring(url.indexOf("://") + 3);
File root = Toolkit.getMxRoot();
File gitAliases = new File(root, "git.moxie");
if (gitAliases.exists()) {
try {
MaxmlMap map = Maxml.parse(gitAliases);
// look for protocol alias matches
for (String alias : map.keySet()) {
// ensure alias is legal
if ("ftp".equals(alias)
|| "http".equals(alias)
|| "https".equals(alias)
|| "git".equals(alias)
|| "ssh".equals(alias)) {
error(MessageFormat.format("Illegal repository alias \"{0}\"!", alias));
continue;
}
// look for alias match
String proto = alias + "://";
if (url.startsWith(proto)) {
String baseUrl = map.getString(alias, "");
if (baseUrl.charAt(baseUrl.length() - 1) != '/') {
return baseUrl + '/' + repo;
}
return baseUrl + repo;
}
}
} catch (Exception e) {
throw new MoxieException(e);
}
}
return url;
}
private void log(String msg) {
System.out.println(msg);
}
private void error(String msg) {
System.err.println(msg);
}
private enum Eclipse {
none, user, var, ext;
boolean includeProject() {
return this.ordinal() > none.ordinal();
}
boolean includeClasspath() {
return this.ordinal() > user.ordinal();
}
}
private enum IntelliJ {
none, var;
boolean includeProject() {
return this.ordinal() > none.ordinal();
}
boolean includeClasspath() {
return true;
}
}
private class NewProject {
String type = "jar";
String groupId = "mygroup";
String artifactId = "artifact";
String version = "0.0.0-SNAPSHOT";
boolean initGit = false;
String gitOrigin;
Eclipse eclipse = Eclipse.none;
IntelliJ idea = IntelliJ.none;
File dir;
}
@Override
public void buildStarted(BuildEvent event) {
}
@Override
public void buildFinished(BuildEvent event) {
if (newProject != null && newProject.initGit) {
// init Git repository after running moxie.init
try {
initGit();
} catch (GitAPIException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void targetStarted(BuildEvent event) {
}
@Override
public void targetFinished(BuildEvent event) {
}
@Override
public void taskStarted(BuildEvent event) {
}
@Override
public void taskFinished(BuildEvent event) {
}
@Override
public void messageLogged(BuildEvent event) {
}
}