/* *******************************************************************
 * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Eclipse Public License v1.0 
 * which accompanies this distribution and is available at 
 * http://www.eclipse.org/legal/epl-v10.html 
 *  
 * Contributors: 
 *     Xerox/PARC     initial implementation 
 * ******************************************************************/
package org.aspectj.internal.tools.ant.taskdefs;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Javac;
import org.apache.tools.ant.taskdefs.Zip;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.ZipFileSet;
import org.aspectj.internal.tools.build.BuildSpec;
import org.aspectj.internal.tools.build.Builder;
import org.aspectj.internal.tools.build.Messager;
import org.aspectj.internal.tools.build.Module;
import org.aspectj.internal.tools.build.Result;
import org.aspectj.internal.tools.build.Util;

/**
 * Implement Builder in Ant.
 */
public class AntBuilder extends Builder {
	private static final boolean FORCE_FORK_FOR_LIBRARIES = false;

	/**
	 * Factory for a Builder.
	 * 
	 * @param config the String configuration, where only substrings "verbose" and "useEclipseCompiles" are significant
	 * @param project the owning Project for all tasks (not null)
	 * @param tempDir the File path to a temporary dir for side effects (may be null)
	 * @return a Builder for this project and configuration
	 */
	public static Builder getBuilder(String config, Project project, File tempDir) {
		boolean useEclipseCompiles = false;
		boolean verbose = false;
		if (null != config) {
			if (-1 != config.indexOf("useEclipseCompiles")) {
				useEclipseCompiles = true;
			}
			if (-1 != config.indexOf("verbose")) {
				verbose = true;
			}
		}
		// Messager handler = new Messager(); // debugging
		Messager handler = new ProjectMessager(project);
		Builder result = new ProductBuilder(project, tempDir, useEclipseCompiles, handler);
		if (verbose) {
			result.setVerbose(true);
		}
		return result;
	}

	private static String resultToTargetName(Result result) {
		return result.getName();
	}

	/**
	 * Ensure targets exist for this module and all antecedants, so topoSort can work.
	 */
	private static void makeTargetsForResult(final Result result, final Hashtable<String,Target> targets) {
		final String resultTargetName = resultToTargetName(result);
		Target target = targets.get(resultTargetName);
		if (null == target) {
			// first add the target
			target = new Target();
			target.setName(resultTargetName);

			Result[] reqs = result.getRequired();
			StringBuffer depends = new StringBuffer();
			boolean first = true;
			for (int i = 0; i < reqs.length; i++) {
				Result reqResult = reqs[i];
				if (!first) {
					depends.append(",");
				} else {
					first = false;
				}
				depends.append(resultToTargetName(reqResult));
			}
			if (0 < depends.length()) {
				target.setDepends(depends.toString());
			}
			targets.put(resultTargetName, target);

			// then recursively add any required results
			for (int i = 0; i < reqs.length; i++) {
				Result reqResult = reqs[i];
				makeTargetsForResult(reqResult, targets);
			}
		}
	}

	private final Project project;

	protected AntBuilder(Project project, File tempDir, boolean useEclipseCompiles, Messager handler) {
		super(tempDir, useEclipseCompiles, handler);
		this.project = project;
		Util.iaxIfNull(project, "project");
	}

	/**
	 * Initialize task with project and "ajbuild-" + name as name. (Using bm- prefix distinguishes these tasks from tasks found in
	 * the build script.)
	 * 
	 * @param task the Task to initialize - not null
	 * @param name the String name suffix for the task
	 * @return true unless some error
	 */
	protected boolean setupTask(Task task, String name) {
		task.setProject(project);
		task.setTaskName("ajbuild-" + name);
		return true;
	}

	/**
	 * Copy file, optionally filtering. (Filters set in project.)
	 * 
	 * @param fromFile the readable File source to copy
	 * @param toFile the writable File destination file
	 * @param boolean filter if true, enable filtering
	 * @see org.aspectj.internal.tools.build.Builder#copyFile(File, File, boolean)
	 */
	@Override
	protected boolean copyFile(File fromFile, File toFile, boolean filter) {
		Copy copy = makeCopyTask(filter);
		copy.setFile(fromFile);
		copy.setTofile(toFile);
		executeTask(copy);
		return true;
	}

	/**
	 * (Filters set in project.)
	 * 
	 * @see org.aspectj.internal.tools.ant.taskdefs.Builder#copyFiles(File, File, String, String, boolean)
	 */
	@Override
	protected boolean copyFiles(File fromDir, File toDir, String includes, String excludes, boolean filter) {
		Copy copy = makeCopyTask(filter);
		copy.setTodir(toDir);
		FileSet fileset = new FileSet();
		fileset.setDir(fromDir);
		if (null != includes) {
			fileset.setIncludes(includes);
		}
		if (null != excludes) {
			fileset.setExcludes(excludes);
		}
		copy.addFileset(fileset);
		executeTask(copy);

		return false;
	}

	protected void copyFileset(File toDir, FileSet fileSet, boolean filter) {
		Copy copy = makeCopyTask(filter);
		copy.addFileset(fileSet);
		copy.setTodir(toDir);
		executeTask(copy);
	}

	/**
	 * @param filter if FILTER_ON, use filters
	 */
	protected Copy makeCopyTask(boolean filter) {
		Copy copy = new Copy();
		setupTask(copy, "copy");
		if (FILTER_ON == filter) {
			copy.setFiltering(true);
		}
		return copy;
	}

	protected void dumpMinFile(Result result, File classesDir, List<String> errors) {
		String name = result.getName() + "-empty";
		File minFile = new File(classesDir, name);
		FileWriter fw = null;
		try {
			fw = new FileWriter(minFile);
			fw.write(name);
		} catch (IOException e) {
			errors.add("IOException writing " + name + " to " + minFile + ": " + Util.renderException(e));
		} finally {
			Util.close(fw);
		}

	}

	@Override
	protected boolean compile(Result result, File classesDir, boolean useExistingClasses, List<String> errors) {
		if (!classesDir.exists() && !classesDir.mkdirs()) {
			errors.add("compile - unable to create " + classesDir);
			return false;
		}
		if (useExistingClasses) {
			return true;
		}
		// -- source paths
		Path path = new Path(project);
		boolean hasSourceDirectories = false;
		boolean isJava5Compile = false;
		boolean isJava8Compile = false;
		for (File file: result.getSrcDirs()) {
			path.createPathElement().setLocation(file);
			if (!isJava5Compile
					&& (Util.Constants.JAVA5_SRC.equals(file.getName()) || 
						Util.Constants.JAVA5_TESTSRC.equals(file.getName()) ||
						new File(file.getParent(), ".isJava5").exists())) {
				isJava5Compile = true;
			}
			if (new File(file.getParent(),".isJava8").exists()) {
				isJava8Compile = true;
			}
			if (!hasSourceDirectories) {
				hasSourceDirectories = true;
			}
		}
		if (!hasSourceDirectories) {
			return true; // nothing to compile - ok
		}
		// XXX test whether build.compiler property takes effect automatically
		// I suspect it requires the proper adapter setup.
		Javac javac = new Javac();
		setupTask(javac, "javac");
		javac.setIncludeantruntime(false);
		javac.setDestdir(classesDir);
		javac.setSrcdir(path);
		javac.setVerbose(verbose);
		path = null;

		// -- classpath
		Path classpath = new Path(project);
		boolean hasLibraries = setupClasspath(result, classpath);
		if (hasLibraries && FORCE_FORK_FOR_LIBRARIES) {
			javac.setFork(true); // otherwise never releases library jars
			// can we build under 1.4, but fork javac 1.5 compile?
		}
		// also fork if using 1.5?

		// -- set output directory
		classpath.createPathElement().setLocation(classesDir);
		javac.setClasspath(classpath);

		// misc
		javac.setDebug(true);
		if (isJava8Compile) {
			javac.setSource("1.8");
			javac.setTarget("1.8");				
		} else if (isJava5Compile) {
			// *cough*
			javac.setSource("1.6");
			javac.setTarget("1.6");			
		} else {
			javac.setTarget("1.1"); // 1.1 class files - Javac in 1.4 uses 1.4
			javac.setSource("1.3");
		}
		// compile
		boolean passed = false;
		BuildException failure = null;
		try {
			passed = executeTask(AspectJSupport.wrapIfNeeded(result, javac));
		} catch (BuildException e) {
			failure = e;
		} catch (Error e) {
			failure = new BuildException(e);
		} catch (RuntimeException e) {
			failure = new BuildException(e);
		} finally {
			if (!passed) {
				String args = "" + Arrays.asList(javac.getCurrentCompilerArgs());
				if ("[]".equals(args)) {
					args = "{" + result.toLongString() + "}";
				}
				String m = "BuildException compiling " + result.toLongString() + args
						+ (null == failure ? "" : ": " + Util.renderException(failure));
				// debuglog System.err.println(m);
				errors.add(m);
			}
			javac.init(); // be nice to let go of classpath libraries...
		}
		return passed;
	}

	public boolean setupClasspath(Result result, Path classpath) { // XXX fix test access
		boolean hasLibraries = false;
		// required libraries
		for (Iterator iter = result.getLibJars().iterator(); iter.hasNext();) {
			File file = (File) iter.next();
			classpath.createPathElement().setLocation(file);
			if (!hasLibraries) {
				hasLibraries = true;
			}
		}
		// Westodo Kind kind = result.getKind();
		Result[] reqs = result.getRequired();
		// required modules and their exported libraries
		for (int i = 0; i < reqs.length; i++) {
			Result requiredResult = reqs[i];
			classpath.createPathElement().setLocation(requiredResult.getOutputFile());
			if (!hasLibraries) {
				hasLibraries = true;
			}
			// also put on classpath libraries exported from required module
			// XXX exported modules not supported
			for (Iterator iterator = requiredResult.getExportedLibJars().iterator(); iterator.hasNext();) {
				classpath.createPathElement().setLocation((File) iterator.next());
			}
		}
		return hasLibraries;
	}

	/**
	 * Merge classes directory and any merge jars into module jar with any specified manifest file. META-INF directories are
	 * excluded.
	 */
	@Override
	protected boolean assemble(Result result, File classesDir, List<String> errors) {
		if (!buildingEnabled) {
			return false;
		}
		if (!result.outOfDate()) {
			return true;
		}

		// ---- zip result up
		Zip zip = new Zip();
		setupTask(zip, "zip");
		zip.setDestFile(result.getOutputFile());
		ZipFileSet zipfileset = null;

		// -- merge any resources in any of the src directories
		for (Iterator iter = result.getSrcDirs().iterator(); iter.hasNext();) {
			File srcDir = (File) iter.next();
			zipfileset = new ZipFileSet();
			zipfileset.setProject(project);
			zipfileset.setDir(srcDir);
			zipfileset.setIncludes(RESOURCE_PATTERN);
			zip.addZipfileset(zipfileset);
		}

		final Module module = result.getModule();

		File metaInfDir = new File(classesDir, "META-INF");
		Util.deleteContents(metaInfDir);

		// -- manifest
		File manifest = new File(module.moduleDir, module.name + ".mf.txt"); // XXXFileLiteral
		if (Util.canReadFile(manifest)) {
			if (Util.canReadDir(metaInfDir) || metaInfDir.mkdirs()) {
				// Jar spec requires a MANIFEST.MF not a manifest.mf
				copyFile(manifest, new File(metaInfDir, "MANIFEST.MF"), FILTER_ON); // XXXFileLiteral
			} else {
				errors.add("have manifest, but unable to create " + metaInfDir);
				return false;
			}
		}

		zipfileset = new ZipFileSet();
		zipfileset.setProject(project);
		zipfileset.setDir(classesDir);
		zipfileset.setIncludes("**/*");
		zip.addZipfileset(zipfileset);
		File[] contents = classesDir.listFiles();
		if ((null == contents) || (0 == contents.length)) {
			// *something* to zip up
			dumpMinFile(result, classesDir, errors);
		}

		try {
			handler.log("assemble " + module + " in " + result.getOutputFile());
			return executeTask(zip)
			// zip returns true when it doesn't create zipfile
					// because there are no entries to add, so verify done
					&& Util.canReadFile(result.getOutputFile());
		} catch (BuildException e) {
			errors.add("BuildException zipping " + module + ": " + e.getMessage());
			return false;
		} finally {
			result.clearOutOfDate();
		}
	}

	/**
	 * @see org.aspectj.internal.tools.build.Builder#buildAntecedants(Module)
	 */
	@Override
	protected Result[] getAntecedantResults(Result moduleResult) {
		Hashtable<String,Target> targets = new Hashtable<String, Target>();
		makeTargetsForResult(moduleResult, targets);
		String targetName = resultToTargetName(moduleResult);
		// bug: doc says topoSort returns String, but returns Target
		Collection<Target> result = project.topoSort(targetName, targets);
		// fyi, we don't rely on topoSort to detect cycles - see buildAll
		int size = result.size();
		if (0 == result.size()) {
			return new Result[0];
		}
		ArrayList<String> toReturn = new ArrayList<String>();
		for (Iterator<Target> iter = result.iterator(); iter.hasNext();) {
			Target target = iter.next();
			String name = target.getName();
			if (null == name) {
				throw new Error("null name?");
			} else {
				toReturn.add(name);
			}
		}
		// topoSort always returns target name
		if ((1 == size) && targetName.equals(toReturn.get(0)) && !moduleResult.outOfDate()) {
			return new Result[0];
		}
		return Result.getResults(toReturn.toArray(new String[0]));
	}

	/**
	 * Generate Module.assembledJar with merge of itself and all antecedants
	 */
	@Override
	protected boolean assembleAll(Result result, Messager handler) {
		if (!buildingEnabled) {
			return false;
		}
		if (!result.outOfDate()) {
			return true;
		}

		Util.iaxIfNull(result, "result");
		Util.iaxIfNull(handler, "handler");
		if (!result.getKind().isAssembly()) {
			throw new IllegalStateException("not assembly: " + result);
		}

		// ---- zip result up
		Zip zip = new Zip();
		setupTask(zip, "zip");
		zip.setDestFile(result.getOutputFile());
		ZipFileSet zipfileset = null;
		final Module module = result.getModule();
		List<File> known = result.findJarRequirements();
		removeLibraryFilesToSkip(module, known);
		// -- merge any antecedents, less any manifest
		for (File jarFile: known) {
			zipfileset = new ZipFileSet();
			zipfileset.setProject(project);
			zipfileset.setSrc(jarFile);
			zipfileset.setIncludes("**/*");
			String name = jarFile.getName();
			name = name.substring(0, name.length() - 4); // ".jar".length()
			// required includes self - exclude manifest from others
			if (!module.name.equals(name)) {
				zipfileset.setExcludes("META-INF/MANIFEST.MF"); // XXXFileLiteral
				zipfileset.setExcludes("META-INF/manifest.mf");
				zipfileset.setExcludes("meta-inf/manifest.mf");
				zipfileset.setExcludes("meta-inf/MANIFEST.MF");
			}
			zip.addZipfileset(zipfileset);
		}

		try {
			handler.log("assembling all " + module + " in " + result.getOutputFile());
			if (verbose) {
				handler.log("knownAntecedants: " + known);
			}
			return executeTask(zip);
		} catch (BuildException e) {
			handler.logException("BuildException zipping " + module, e);
			return false;
		} finally {
			result.clearOutOfDate();
		}
	}

	/**
	 * @see org.aspectj.internal.tools.ant.taskdefs.Builder#buildInstaller(BuildSpec, String)
	 */
	@Override
	protected boolean buildInstaller(BuildSpec buildSpec, String targDirPath) {
		return false;
	}

	/** task.execute() and any advice */
	protected boolean executeTask(Task task) {
		if (!buildingEnabled) {
			return false;
		}
		task.execute();
		return true;
	}

	/**
	 * Support for compiling basic AspectJ projects. Projects may only compile all (and only) their source directories; aspectpath,
	 * inpath, etc. are not supported. To load the compiler, this assumes the user has either defined a project property
	 * "aspectj.home" or that there exists <code>{module-dir}/lib/aspectj/lib/aspectj[tools|rt].jar</code>.
	 */
	static class AspectJSupport {
		static final String AJCTASK = "org.aspectj.tools.ant.taskdefs.AjcTask";
		static final String ASPECTJRT_JAR_VARIABLE = "ASPECTJRT_LIB";
		static final String LIBASPECTJ_RPATH = "/lib/aspectj";
		static final Map nameToAspectjrtjar = new HashMap();
		static final String NONE = "NONE";

		/**
		 * If this module should be compiled with AspectJ, return a task to do so.
		 * 
		 * @param module the Module to compile
		 * @param javac the Javac compile commands
		 * @return javac or a Task to compile with AspectJ if needed
		 */
		static Task wrapIfNeeded(Result result, Javac javac) {
			final Project project = javac.getProject();
			Path runtimeJar = null;
			final Module module = result.getModule();
			if (runtimeJarOnClasspath(result)) {
				// yes aspectjrt.jar on classpath
			} else if (result.getClasspathVariables().contains(ASPECTJRT_JAR_VARIABLE)) {
				// yes, in variables - find aspectjrt.jar to add to classpath
				runtimeJar = getAspectJLib(project, module, "aspectjrt.jar");
			} else {
				// no
				// System.out.println("javac " + result + " " + javac.getClasspath());
				return javac;
			}
			// System.out.println("aspectj " + result + " " + javac.getClasspath());
			Path aspectjtoolsJar = getAspectJLib(project, module, "aspectjtools.jar");
			return aspectJTask(javac, aspectjtoolsJar, runtimeJar);
		}

		/** @return true if aspectjrt.jar is on classpath */
		private static boolean runtimeJarOnClasspath(Result result) {
			for (File file: result.getLibJars()) {
				if ("aspectjrt.jar".equals(file.getName())) {
					return true;
				}
			}
			return false;
		}

		static Path getAspectJLib(Project project, Module module, String name) {
			Path result = null;
			String[] libDirNames = { "aspectj.home", "ASPECTJ_HOME", LIBASPECTJ_RPATH };
			String[] libDirs = new String[libDirNames.length];
			for (int i = 0; i < libDirNames.length; i++) {
				if (LIBASPECTJ_RPATH == libDirNames[i]) {
					libDirs[i] = module.getFullPath(LIBASPECTJ_RPATH);
				} else {
					libDirs[i] = project.getProperty(libDirNames[i]);
				}
				if (null != libDirs[i]) {
					libDirs[i] = Util.path(libDirs[i], "lib");
					result = new Path(project, Util.path(libDirs[i], name));
					String path = result.toString();
					if (new File(path).canRead()) {
						return result;
					}
				}
			}
			String m = "unable to find " + name + " in " + Arrays.asList(libDirs);
			throw new BuildException(m);
		}

		/**
		 * Wrap AspectJ compiler as Task. Only works for javac-like source compilation of everything under srcDir. Written
		 * reflectively to compile in the build module, which can't depend on the whole tree.
		 * 
		 * @param javac the Javac specification
		 * @param toolsJar the Path to the aspectjtools.jar
		 * @param runtimeJar the Path to the aspectjrt.jar
		 * @return javac or another Task invoking the AspectJ compiler
		 */
		@SuppressWarnings("unchecked")
		static Task aspectJTask(Javac javac, Path toolsJar, Path runtimeJar) {
			Object task = null;
			String url = null;
			try {
				url = "file:" + toolsJar.toString().replace('\\', '/');
				URL[] cp = new URL[] { new URL(url) };
				ClassLoader parent = Task.class.getClassLoader();
				ClassLoader loader = new URLClassLoader(cp, parent);
				Class c = loader.loadClass(AJCTASK);
				task = c.newInstance();
				// Westodo Project project = javac.getProject();
				Method m = c.getMethod("setupAjc", new Class[] { Javac.class });
				m.invoke(task, new Object[] { javac });
				m = c.getMethod("setFork", new Class[] { boolean.class });
				m.invoke(task, new Object[] { Boolean.TRUE });
				m = c.getMethod("setForkclasspath", new Class[] { Path.class });
				m.invoke(task, new Object[] { toolsJar });
				m = c.getMethod("setSourceRoots", new Class[] { Path.class });
				m.invoke(task, new Object[] { javac.getSrcdir() });
				if (null != runtimeJar) {
					m = c.getMethod("setClasspath", new Class[] { Path.class });
					m.invoke(task, new Object[] { runtimeJar });
				}
			} catch (BuildException e) {
				throw e;
			} catch (Throwable t) {
				StringBuffer sb = new StringBuffer();
				sb.append("classpath=");
				sb.append(url);
				throw new BuildException(sb.toString(), t);
			}
			return (Task) task;
		}

		private AspectJSupport() {
			throw new Error("no instances");
		}
	}
}

// finally caught by failing to comply with proper ant initialization
// /**
// * Build a module that has a build script.
// * @param buildSpec the module to build
// * @param buildScript the script file
// * @throws BuildException if build fails
// */
// private void buildByScript(BuildSpec buildSpec, File buildScript)
// throws BuildException {
// Ant ant = new Ant();
// ant.setProject(getProject());
// ant.setAntfile(buildScript.getAbsolutePath());
// ant.setDescription("building module " + buildSpec.module);
// ant.setDir(buildScript.getParentFile());
// ant.setInheritAll(true);
// ant.setInheritRefs(false);
// ant.setLocation(getLocation());
// ant.setOwningTarget(getOwningTarget());
// // by convention, for build.xml, use module name to publish
// ant.setTarget(buildSpec.module);
// ant.setTaskName("ant");
// loadAntProperties(ant, buildSpec);
// ant.execute();
// }
//     
// /** override definitions */
// private void loadAntProperties(Ant ant, BuildSpec buildSpec) {
// Property property = ant.createProperty();
// property.setName(BuildSpec.baseDir_NAME);
// property.setFile(buildSpec.baseDir);
// property = ant.createProperty();
// property.setName(buildSpec.distDir_NAME);
// property.setFile(buildSpec.distDir);
// property = ant.createProperty();
// property.setName(BuildSpec.tempDir_NAME);
// property.setFile(buildSpec.tempDir);
// property = ant.createProperty();
// property.setName(BuildSpec.jarDir_NAME);
// property.setFile(buildSpec.jarDir);
// property = ant.createProperty();
// property.setName(BuildSpec.stagingDir_NAME);
// property.setFile(buildSpec.stagingDir);
// }

/**
 * Segregate product-building API's from module-building APIs for clarity. These are called by the superclass if the BuildSpec
 * warrants. XXX extremely brittle/arbitrary assumptions.
 * 
 * @see BuildModule for assumptions
 */
class ProductBuilder extends AntBuilder {

	private static String getProductInstallResourcesSrc(BuildSpec buildSpec) {
		final String resourcesName = "installer-resources"; // XXXFileLiteral
		File dir = buildSpec.productDir.getParentFile();
		if (null == dir) {
			return Util.path(new String[] { "..", "..", resourcesName });
		}
		dir = dir.getParentFile();
		if (null == dir) {
			return Util.path("..", resourcesName);
		} else {
			dir = new File(dir, resourcesName);
			return dir.getPath();
		}
	}

	private static String getProductInstallerFileName(BuildSpec buildSpec) { // XXXFileLiteral
		return "aspectj-" + buildSpec.productDir.getName() + "-" + Util.shortVersion(buildSpec.version) + ".jar";
	}

	/**
	 * Calculate name of main, typically InitialCap, and hence installer class.
	 * 
	 * @return $$installer$$.org.aspectj." + ProductName + "Installer"
	 */

	private static String getProductInstallerMainClass(BuildSpec buildSpec) {
		String productName = buildSpec.productDir.getName();
		String initial = productName.substring(0, 1).toUpperCase();
		productName = initial + productName.substring(1);
		return "$installer$.org.aspectj." + productName + "Installer"; // XXXNameLiteral
	}

	/** @see Builder.getBuilder(String, Project, File) */
	ProductBuilder(Project project, File tempDir, boolean useEclipseCompiles, Messager handler) {
		super(project, tempDir, useEclipseCompiles, handler);
	}

	/**
	 * Delegate for super.buildProduct(..) template method.
	 */
	@Override
	protected boolean copyBinaries(BuildSpec buildSpec, File distDir, File targDir, String excludes) {
		Copy copy = makeCopyTask(false);
		copy.setTodir(targDir);
		FileSet fileset = new FileSet();
		fileset.setDir(distDir);
		fileset.setIncludes(Builder.BINARY_SOURCE_PATTERN);
		if (null != excludes) {
			fileset.setExcludes(excludes);
		}
		copy.addFileset(fileset);
		return executeTask(copy);
	}

	/**
	 * Delegate for super.buildProduct(..) template method.
	 */
	@Override
	protected boolean copyNonBinaries(BuildSpec buildSpec, File distDir, File targDir) {
		// filter-copy everything but the binaries
		Copy copy = makeCopyTask(true);
		copy.setTodir(targDir);
		Util.iaxIfNotCanReadDir(distDir, "product dist directory");
		FileSet fileset = new FileSet();
		fileset.setDir(distDir);
		fileset.setExcludes(Builder.BINARY_SOURCE_PATTERN);
		copy.addFileset(fileset);
		return executeTask(copy);
	}

	@Override
	protected boolean buildInstaller(BuildSpec buildSpec, String targDirPath) {
		if (buildSpec.verbose) {
			handler.log("creating installer for " + buildSpec);
		}
		AJInstaller installer = new AJInstaller();
		setupTask(installer, "installer");
		installer.setBasedir(targDirPath);
		// installer.setCompress();
		File installSrcDir = new File(buildSpec.productDir, "install"); // XXXFileLiteral
		Util.iaxIfNotCanReadDir(installSrcDir, "installSrcDir");
		installer.setHtmlSrc(installSrcDir.getPath());
		String resourcePath = getProductInstallResourcesSrc(buildSpec);
		File resourceSrcDir = new File(resourcePath);
		Util.iaxIfNotCanReadDir(resourceSrcDir, "resourceSrcDir");
		installer.setResourcesSrc(resourcePath);
		String name = getProductInstallerFileName(buildSpec);
		File outFile = new File(buildSpec.jarDir, name);
		installer.setZipfile(outFile.getPath());
		installer.setMainclass(getProductInstallerMainClass(buildSpec));
		installer.setInstallerclassjar(getBuildJar(buildSpec));
		return executeTask(installer);

		// -- test installer XXX
		// create text setup file
		// run installer with setup file
		// cleanup installed product
	}

	private String getBuildJar(BuildSpec buildSpec) {
		return buildSpec.baseDir.getPath() + "/lib/build/build.jar"; // XXX
	}

	// private Module moduleForReplaceFile(File replaceFile, Modules modules) {
	// String jarName = moduleAliasFor(replaceFile.getName().toLowerCase());
	// if (jarName.endsWith(".jar") || jarName.endsWith(".zip")) { // XXXFileLiteral
	// jarName = jarName.substring(0, jarName.length()-4);
	// } else {
	// throw new IllegalArgumentException("can only replace .[jar|zip]");
	// }
	// boolean assembleAll = jarName.endsWith("-all");
	// String name = (!assembleAll ? jarName : jarName.substring(0, jarName.length()-4));
	// return modules.getModule(name);
	// }
	//    
}

class ProjectMessager extends Messager {
	private final Project project;

	public ProjectMessager(Project project) {
		Util.iaxIfNull(project, "project");
		this.project = project;
	}

	@Override
	public boolean log(String s) {
		project.log(s);
		return true;
	}

	@Override
	public boolean error(String s) {
		project.log(s, Project.MSG_ERR);
		return true;
	}

	@Override
	public boolean logException(String context, Throwable thrown) {
		project.log(context + Util.renderException(thrown), Project.MSG_ERR);
		return true;
	}

}