/* * 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) { } }