// AntModel.java
//
// Created: November 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.dashboard;

import org.tulsoft.shared.UUtils;

import org.biomoby.shared.MobyException;
import org.biomoby.service.dashboard.SimpleAnt;

import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.ExitException;
import org.apache.tools.ant.NoBannerLogger;

import java.io.PrintStream;
import java.io.OutputStream;
import java.util.Vector;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Map;
import java.util.Iterator;
import java.io.File;

/**
 * A model using Ant to achieve its tasks. <p>
 *
 * The methods are simple because most of their parameters and
 * properties are not given to them directly but through a property
 * channel where they can be retrieved in due time. This is hopefully
 * more flexible because the number of configurable properties may
 * be changed significantly during the time. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: AntModel.java,v 1.10 2009/02/03 09:02:28 groscurt Exp $
 */

public class AntModel
	extends AbstractModel {
	
	private static org.apache.commons.logging.Log log =
	   org.apache.commons.logging.LogFactory.getLog (AntModel.class);
	
	// few delegated constants
	public final static int MSG_QUIET   = 100;
	public final static int MSG_INFO    = Project.MSG_INFO;
	public final static int MSG_VERBOSE = Project.MSG_VERBOSE;
	public final static int MSG_DEBUG   = Project.MSG_DEBUG;
	
	// to have access to a Biomoby registry (and to the local cache of
	// Biomoby entities)
	protected RegistryModel registryModel;
	
	// component for running Ant
	protected OutputStream outputStream = null;
	protected OutputStream errorStream = null;
	protected int msgOutputLevel = -1;
	
	/*********************************************************************
	 * Default constructor.
	 ********************************************************************/
	public AntModel() {
	super();
	
	}
	
	/*********************************************************************
	 * A usual constructor. The registry model connect this model with
	 * a Biomoby registry and with a local cache of Biomoby entities.
	 ********************************************************************/
	public AntModel (RegistryModel registryModel) {
	super();
	this.registryModel = registryModel;
	}
	
	/*********************************************************************
	 * Set output stream where running Ant will be reporting.
	 ********************************************************************/
	public void setOutputStream (OutputStream os) {
	outputStream = os;
	}
	
	/*********************************************************************
	 * Set output stream where running Ant will be reporting errors.
	 ********************************************************************/
	public void setErrorStream (OutputStream os) {
	errorStream = os;
	}
	
	/*********************************************************************
	 *
	 ********************************************************************/
	public void setMsgOutputLevel (int level) {
	msgOutputLevel = level;
	}
	
	/*********************************************************************
	 * Utility to create a filename from pieces, independently on a
	 * platform-specific file separator.
	 ********************************************************************/
	public static File createFileName (String[] parts) {
	if (parts == null || parts.length == 0)
	    return new File (System.getProperty ("user.dir"));
	File f = new File (parts[0]);
	for (int i = 1; i < parts.length; i++)
	    f = new File (f, parts[i]);
	return f;
	}
	
	/*********************************************************************
	 * Deal with Moses-generated datatypes. It may include generating,
	 * compiling, documeting and jarring them. 
	 ********************************************************************/
	public void mosesDatatypes()
	throws MobyException {
	
	boolean toGenerate = propertyChannel.getBoolean (DP_USE_DT_GEN, true);
	boolean toCompile = propertyChannel.getBoolean (DP_USE_DT_COMP, true);
	boolean toJavadoc = propertyChannel.getBoolean (DP_USE_DT_DOC, true);
	boolean toJar = propertyChannel.getBoolean (DP_USE_DT_JAR, true);
	
	// build a set of properties for Ant
	Properties props = new Properties();
	addRegistryProps (props);
	addGeneratorProps (props);
	
	// build a set of Ant targets
	Vector<String> v = new Vector<String>();
	if (toGenerate && toCompile && toJavadoc && toJar) {
	    v.addElement ("moses-datatypes");
	} else {
	    if (toGenerate)
		v.addElement ("generate-datatypes");
	    if (toJavadoc)
		v.addElement ("moses-docs");
	    if (toJar)
		v.addElement ("jar-datatypes");
	    if (toCompile && ! toJavadoc && ! toJar)
		v.addElement ("moses-compile");
	}
	String[] targets = new String [v.size()];
	v.copyInto (targets);
	
	// run it...
	runAnt (targets, props);
	}
	
	/*********************************************************************
	 * Remove generated datatypes.
	 ********************************************************************/
	public void mosesCleanDatatypes()
	throws MobyException {
	runAnt (new String[] { "clean-datatypes" }, new Properties());
	}
	
	/*********************************************************************
	 * Deal with Moses-generated skeletons. It may include generating,
	 * compiling, documeting and jarring them. 
	 ********************************************************************/
	public void mosesSkeletons()
	throws MobyException {
	
	// check whether datatypes exist (if not, generate them)
	File f = createFileName (new String[] {
	    System.getProperty ("user.dir"),
	    "build", "others", "datatypes", "org", "biomoby", "shared", "datatypes",
	    "MapDataTypes.class" });
	if ( ! f.exists()) {
	    boolean origToGenerate = propertyChannel.getBoolean (DP_USE_DT_GEN, true);
	    boolean origToCompile = propertyChannel.getBoolean (DP_USE_DT_COMP, true);
	    boolean origToJavadoc = propertyChannel.getBoolean (DP_USE_DT_DOC, true);
	    boolean origToJar = propertyChannel.getBoolean (DP_USE_DT_JAR, true);
	    try {
		propertyChannel.put (DP_USE_DT_GEN, Boolean.TRUE.toString());
		propertyChannel.put (DP_USE_DT_COMP, Boolean.TRUE.toString());
		propertyChannel.put (DP_USE_DT_DOC, Boolean.FALSE.toString());
		propertyChannel.put (DP_USE_DT_JAR, Boolean.FALSE.toString());
		mosesDatatypes();
	    } finally {
		propertyChannel.put (DP_USE_DT_GEN, new Boolean (origToGenerate).toString());
		propertyChannel.put (DP_USE_DT_COMP, new Boolean (origToCompile).toString());
		propertyChannel.put (DP_USE_DT_DOC, new Boolean (origToJavadoc).toString());
		propertyChannel.put (DP_USE_DT_JAR, new Boolean (origToJar).toString());
	    }
	}
	
	// build a set of properties for Ant
	Properties props = new Properties();
	addRegistryProps (props);
	addGeneratorProps (props);
	
	// TBD
	//	String mosesFlavour = propertyChannel.getString (DP_MOSES_FLAVOUR);
	
	// filtering by authorities and/or by service names
	filterAndRun (props, DP_SEL_AUTHORITIES, "moses.authority");
	props.remove ("moses.authority");
	filterAndRun (props, DP_SEL_SERVICES, "moses.service");
	props.remove ("moses.service");
	}
	
	/*********************************************************************
	 * Deploy selected services to a server (Tomcat/Axis).
	 ********************************************************************/
	public void mosesDeploy()
	throws MobyException {
	
	// build a set of properties for Ant
	Properties props = new Properties();
	addDeploymentProps (props);
	add (props, "users.lib.dir", DP_USER_JARS);
	add (props, "wsdd.template", DP_WSDD_TEMPL);
	
	// run it...
	if (propertyChannel.getBoolean (DP_LOCAL_DEPLOY, true))
	    runAnt (new String[] { "deploy-services" }, props);
	else
	    runAnt (new String[] { "deploy-remote" }, props);
	}
	
	/*********************************************************************
	 * Undeploy selected services to a server (Tomcat/Axis).
	 ********************************************************************/
	public void mosesUndeploy()
	throws MobyException {
	
	// build a set of properties for Ant
	Properties props = new Properties();
	addDeploymentProps (props);
	
	// run it...
	if (propertyChannel.getBoolean (DP_LOCAL_DEPLOY, true))
	    runAnt (new String[] { "undeploy-services" }, props);
	}
	
	/*********************************************************************
	 * ...run Ant for skeleton.
	 *
	 * Special case is this: service names come in format:
	 * "service/authority". If there is more than one such name, the
	 * authority part is simply forgotten (and no filter for authority
	 * is created). Such scenario, however, would not work for rare
	 * cases where a service name is not unique. For such cases,
	 * generate only with one value of "service/authority" - and this
	 * is the special case where both filters are setup.
	 ********************************************************************/
	private void filterAndRun (Properties props,
			       String channelNameForFilter,
			       String antNameForFilter)
	throws MobyException {
	
	boolean toGenerate = propertyChannel.getBoolean (DP_USE_S_GEN, true);
	boolean toCompile = propertyChannel.getBoolean (DP_USE_S_COMP, true);
	boolean toJavadoc = propertyChannel.getBoolean (DP_USE_S_DOC, true);
	boolean toJar = propertyChannel.getBoolean (DP_USE_S_JAR, true);
	
	Vector x = (Vector)propertyChannel.get (channelNameForFilter);
	if (x == null || x.size() == 0)
	    return;
	boolean specialCase = (x.size() == 1);
	String authority = null;
	StringBuffer buf = new StringBuffer();
	for (Enumeration en = x.elements(); en.hasMoreElements(); ) {
	    if (buf.length() > 0)
		buf.append ("|");
	    String value = (String)en.nextElement();
	    String[] parts = value.split ("/", 2);   // sorry, bad hack... (originated in CommonNode not having 'value' as Object)
	    if (parts.length > 1 && specialCase)
		authority = parts[1];
	    buf.append ("^");
	    buf.append (parts[0]);
	    buf.append ("$");
	}
	if (authority == null) {
	    add2 (props, antNameForFilter, new String (buf));
	} else {
	    add2 (props, antNameForFilter, new String (buf));
	    add2 (props, "moses.authority", authority);
	}
	
	// build a set of Ant targets
	Vector<String> v = new Vector<String>();
	if (toGenerate && toCompile && toJavadoc && toJar) {
	    v.addElement ("moses-services");
	} else {
	    if (toGenerate)
		v.addElement ("generate-services");
	    if (toJavadoc)
		v.addElement ("moses-docs");
	    if (toJar)
		v.addElement ("jar-services");
	    if (toCompile && ! toJavadoc && ! toJar)
		v.addElement ("moses-compile");
	}
	String[] targets = new String [v.size()];
	v.copyInto (targets);
	
	// run it...
	runAnt (targets, props);
	}
	
	/*********************************************************************
	 * Remove generated skeletons
	 ********************************************************************/
	public void mosesCleanSkeletons()
	throws MobyException {
	runAnt (new String[] { "clean-services" }, new Properties());
	}
	
	/*********************************************************************
	 * Deal both with Moses-generated datatypes and skeletons - in an
	 * appropriate order.
	 ********************************************************************/
	public void mosesAll()
	throws MobyException {
	}
	
	/*********************************************************************
	 * Run Ant...
	 ********************************************************************/
	protected void runAnt (String[] targets, Properties props)
	throws MobyException {
	
	if (targets.length == 0)
	    return;
	
	if (log.isDebugEnabled())
	    log.debug ("Ready to go targets: " +
		       StringUtils.join (targets, ","));
	
	try {
	    SimpleAnt ant = new SimpleAnt();
	    NoBannerLogger logger = new NoBannerLogger();
	    if (outputStream != null)
		logger.setOutputPrintStream (new PrintStream (outputStream));
	    if (errorStream != null)
		logger.setErrorPrintStream (new PrintStream (errorStream));
	    if (msgOutputLevel > -1 && msgOutputLevel != MSG_QUIET)
		ant.setMsgOutputLevel (msgOutputLevel);
	    ant.addBuildListener (logger);
	    ant.startAnt (targets, props);
	
	} catch (ExitException e) {
	    log.warn ("Ant tried to use System.exit() with exit code " +
		      e.getStatus());
	
	} catch (BuildException e) {
	    throw new MobyException (e.getMessage());
	
	} catch (Exception e) {
	    log.error ("Error: " + e.toString());
	    throw new MobyException (e.getMessage(), e);
	}
	}
	
	/*********************************************************************
	 * Add properties about Biomoby registry or a local cache...
	 ********************************************************************/
	protected void addRegistryProps (Properties props) {
	add (props, "default.endpoint", DP_REGISTRY_ENDPOINT);
	add (props, "default.namespace", DP_REGISTRY_NAMESPACE);
	if (propertyChannel.getBoolean (DP_USE_CACHE, true))
	    add (props, "registry.cache.dir", DP_CACHE_DIR);
	}
	
	/*********************************************************************
	 * Add properties shared by both generators...
	 ********************************************************************/
	protected void addGeneratorProps (Properties props) {
	if (msgOutputLevel == MSG_QUIET)
	    add (props, "moses.quiet", true);
	add (props, "moses.nogener",
	     propertyChannel.getBoolean (DP_USE_SIMULATE, false));
	if (! propertyChannel.getBoolean (DP_USE_DOT, true))
	    add (props, "moses.nographs", true);
	add (props, "dot.location", DP_DOT_LOC);
	}
	
	/*********************************************************************
	 * Add properties shared by deployment and undeployment...
	 ********************************************************************/
	protected void addDeploymentProps (Properties props) {
	add (props, "catalina.home", DP_TOMCAT_HOME);
	add (props, "axis.relative.path", DP_AXIS_IN_TOMCAT);
	add (props, "tomcat.host", DP_HOSTNAME);
	add (props, "tomcat.port", DP_PORT);
	add (props, "axis.admin.url", DP_AXIS_ADMIN);
	add (props, "tomcat.webapps", DP_DEPLOY_IN_TOMCAT);
	
	Map services = (Map)propertyChannel.get (DP_DEPL_SERVICES);
	if (services == null) return;
	for (Iterator it = services.entrySet().iterator(); it.hasNext(); ) {
	    Map.Entry entry = (Map.Entry)it.next();
	    String serviceName = (String)entry.getKey();
	    String className = (String)entry.getValue();
	    props.put ("service." + serviceName, className);
	}
	}
	
	/*********************************************************************
	 * Take a property 'channelPropName' from the property channel,
	 * and if it is not empty, add it to 'props' under the name
	 * 'antPropName'.
	 ********************************************************************/
	private void add (Properties props,
		      String antPropName,
		      String channelPropName) {
	String value = (String)propertyChannel.get (channelPropName);
	if (UUtils.notEmpty (value)) {
	    props.put (antPropName, value);
	    if (log.isDebugEnabled())
		log.debug (antPropName + " => " + value);
	}
	}
	
	/*********************************************************************
	 * Similar as above but the value is not from the property channel
	 * but given directly as 'value'.
	 ********************************************************************/
	private void add2 (Properties props,
		       String antPropName,
		       String value) {
	if (UUtils.notEmpty (value)) {
	    props.put (antPropName, value);
	    if (log.isDebugEnabled())
		log.debug (antPropName + " => " + value);
	}
	}
	
	/*********************************************************************
	 * Similar as above but for boolean value (which is not taken from
	 * a property channel but directly given)...
	 ********************************************************************/
	private void add (Properties props,
		      String antPropName,
		      boolean value) {
	props.put (antPropName, new Boolean (value).toString());
	if (log.isDebugEnabled())
	    log.debug (antPropName + " => " + value);
	}

}
