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

package org.biomoby.client;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.Utils;
import org.biomoby.shared.parser.MobyPackage;
import org.biomoby.shared.parser.MobyJob;
import org.biomoby.shared.parser.MobyParser;
import org.biomoby.shared.parser.MobyParameter;
import org.biomoby.shared.datatypes.MobyObject;
import org.biomoby.shared.datatypes.MapDataTypesIfc;

import org.tulsoft.tools.BaseCmdLine;
import org.tulsoft.tools.loaders.ICreator;
import org.tulsoft.shared.GException;
import org.tulsoft.shared.FileUtils;
import org.tulsoft.tools.debug.DGUtils;

import java.util.Vector;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Arrays;
import java.io.IOException;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

/**
 * This is a base class for the command-line driven clients. Such
 * clients are mostly used for testing purposes but some of them can
 * be handy, sitting behind some servers as well. <p>
 *
 * This is not an abstract class - it can be instantiated and directly
 * use (it has its own the {@link #main} method). But it has limited
 * knowledge how to create input data - see its help for more details.
 * For more complex data, you need to subclass it. <p>
 *
 * It also takes care about general-purpose command-like options (that
 * can be used for any service) - such as an <em>endpoint</em> or the
 * <em>help</em> option - see {@link #main} for their list. <p>
 *
 * Another feature is that it can dynamically load a local Java class,
 * instead going to a remote BioMoby service. This can be used for
 * testing new BioMoby services before deploying them. <p>

 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: BaseCmdLineClient.java,v 1.7 2006/05/09 13:32:09 senger Exp $
 */

public class BaseCmdLineClient
    extends BaseClient {

    // dynamically created (org.biomoby.shared.datatypes.MapDataTypes)
    protected MapDataTypesIfc mapDataTypes;

    protected BaseCmdLine cmd;

    // command-line options and arguments
    // (and their default values if they have ones)
    protected boolean exitOnError = true;
    protected boolean stackOnError = true;

    protected String serviceEndpoint;
    protected Class localClass;
    protected String serviceName;
    protected String serviceAuthority;
    protected String registryEndpoint;
    protected String registryURI;
    protected boolean loop = false;

    protected boolean noResults = false;
    protected boolean resultInXML = false;
    protected boolean resultAsString = true;
    protected String outputFile;

    protected int howManyJobs = 1;
    protected int howManyCols = 0;
    protected int howManyHAS = 1;

    protected String divider = ",";
    protected String obj = "Object";
    protected String articleName = "";
    protected Vector ids = new Vector();
    protected Vector ns = new Vector();
    protected Vector values = new Vector();
    protected HashMap namedValues = new HashMap();
    protected HashMap secondaries = new HashMap();

    protected boolean showInputXML = false;
    protected boolean showInputAsString = false;
    protected String inputFile;

    protected boolean sendAsBytes = false;

    /**************************************************************************
     *
     *************************************************************************/
    public BaseCmdLineClient() {
    }

    /**************************************************************************
     *
     *************************************************************************/
    public BaseCmdLineClient (String[] args) {
	cmd = new BaseCmdLine (args, true);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public void reportError (Throwable e) {
	System.err.println ("===ERROR===");
	if (stackOnError) {
	    e.printStackTrace();
	} else {
	    if (e instanceof MobyException)
		System.err.println (e.getMessage());
	    else
		System.err.println (e.toString());
	}
	System.err.println ("===========");
	if (exitOnError)
	    System.exit (1);
    }

    /*************************************************************************
     * Return a help text about this this client.
     *************************************************************************/
    public String getUsage() {
	String helpFile = "help/BaseCmdLineClient_usage.txt";
	try {
	    String help = Utils.readResource (helpFile, this);
	    return (help == null ? "" : help);
	} catch (MobyException e) {
	    return e.getMessage();
	}
    }

    /**************************************************************************
     * TBD: perhaps to split into several overridable methods... (in
     * order to be able to control better how to deal with errors)
     *
     * Return true normally, but false if you suggest not to continue
     * (this is the case when a help was asked, for example).
     *************************************************************************/
    protected boolean parseCmdLine()
	throws MobyException {

	// help?
	if (cmd.hasParam ("-help") || cmd.hasParam ("-h")) {
	    System.out.print (getUsage());
	    return false;
	}

	// basic modes; must be done first before any error can happen
	exitOnError = ! cmd.hasOption ("-noexit");
	stackOnError = ! cmd.hasOption ("-nostack");

	String param;

	// where to go
	loop = cmd.hasOption ("-loop");
	if ( notEmpty (param = cmd.getParam ("-class")) ) {
	    try {
		localClass = Class.forName (param);
	    } catch (ClassNotFoundException e) {
		throw new MobyException ("Class '" + param + "' was not found.", e);
	    }
	}
	serviceEndpoint = cmd.getParam ("-e");
	registryEndpoint = cmd.getParam ("-mobye");
	registryURI = cmd.getParam ("-mobyuri");
	serviceName = cmd.getParam ("-service");
	serviceAuthority = cmd.getParam ("-auth");

	// what to do with the collected input
	showInputAsString = cmd.hasOption ("-show");
	showInputXML = cmd.hasOption ("-showxml");
	if (! loop && isEmpty (serviceName) && localClass == null && ! showInputAsString)
	    showInputXML = true;
	sendAsBytes = cmd.hasOption ("-asbytes");

	// what to do with the result
	noResults = cmd.hasOption ("-noout");
	resultInXML = cmd.hasOption ("-outxml");
	resultAsString = cmd.hasOption ("-outstr");
	outputFile = cmd.getParam ("-o");
	if (! resultInXML && ! resultAsString && ! noResults)
	    resultAsString = true;

	// how many queries/job to create	
	if ( (param = cmd.getParam ("-jobs")) != null )
	    howManyJobs = getInteger (param, "-jobs");

	// ...and how many element in a collection
	if ( (param = cmd.getParam ("-cols")) != null )
	    howManyCols = getInteger (param, "-cols");

	// ...and how many element of HAS children
	if ( (param = cmd.getParam ("-has")) != null )
	    howManyHAS = getInteger (param, "-has");

	// some input data
	inputFile = cmd.getParam ("-xml");
	if ( notEmpty (param = cmd.getParam ("-name")) )
	    articleName = param;
	if ( notEmpty (param = cmd.getParam ("-obj")) )
	    obj = param;
	if ( notEmpty (param = cmd.getParam ("-div")) )
	    divider = param;
	if ( (param = cmd.getParam ("-id")) != null )
	    ids = new Vector (Arrays.asList (param.split (divider)));
	if ( (param = cmd.getParam ("-ns")) != null )
	    ns = new Vector (Arrays.asList (param.split (divider)));
	if ( notEmpty (param = cmd.getParam ("-value")) )
	    values = new Vector (Arrays.asList (param.split (divider)));
	if ( notEmpty (param = cmd.getParam ("-values")) )
	    values = new Vector (Arrays.asList (param.split (divider)));
	if ( notEmpty (param = cmd.getParam ("-str")) ) {
	    obj = "String";
	    values = new Vector (Arrays.asList (param.split (divider)));
	}
	if ( notEmpty (param = cmd.getParam ("-int")) ) {
	    obj = "Integer";
	    values = new Vector (Arrays.asList (param.split (divider)));
	    for (int i = 0; i < values.size(); i++)
		getInteger ((String)values.elementAt (i), "-int[" + i + "]");
	}
	if ( notEmpty (param = cmd.getParam ("-float")) ) {
	    obj = "Float";
	    values = new Vector (Arrays.asList (param.split (divider)));
	    for (int i = 0; i < values.size(); i++)
		getFloat ((String)values.elementAt (i), "-float[" + i + "]");
	}

	for (int i = 0; i < cmd.params.length; i++) {
	    param = cmd.params [i];
	    String[] splitted = param.split ("=", 2);
	    if (splitted.length > 1) {
		if (splitted[0].startsWith ("sec:") && splitted[0].length() > 4) {
		    secondaries.put (splitted [0].substring (4),
				     new Vector (Arrays.asList (splitted [1].split (divider))));
		} else {
		    namedValues.put (splitted [0],
				     new Vector (Arrays.asList (splitted [1].split (divider))));
		}
	    }
	}

	return true;		
    }

    /**************************************************************************
     *
     *************************************************************************/
    int getInteger (String value, String partOfMessage)
	throws MobyException {
	try {
	    return new Integer (value).intValue();
	} catch (NumberFormatException e) {
	    throw new MobyException ("Parameter '" + partOfMessage + "' is not numeric: " + value);
	}
    }

    float getFloat (String value, String partOfMessage)
	throws MobyException {
	try {
	    return new Float (value).floatValue();
	} catch (NumberFormatException e) {
	    throw new MobyException ("Parameter '" + partOfMessage + "' is not numeric: " + value);
	}
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected String shift (Vector data) {
	if (data.size() == 0) return "";
	if (data.size() == 1) return ((String)data.firstElement()).trim();
	return (String)data.remove (0);
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected MobyObject createInstance (String className)
	throws MobyException {
    try {
	    Class theClass = mapDataTypes.getClass (className);
	    if (theClass == null)
		throw new MobyException ("Cannot instantiate data type '" + className + "'. Does it exist?");
	    return (MobyObject)ICreator.createInstance (theClass);
	} catch (GException e) {
	    throw new MobyException (e.getMessage());
	}
    }

    protected MobyObject createInstance (MobyObject mobj)
	throws MobyException {
	try {
	    return (MobyObject)mobj.getClass().newInstance();
	} catch (Exception e) {
	    throw new MobyException ("Cannot instantiate data type '" + mobj.getClass().getName() + "'.");
	}
    }

    protected MobyObject createInstance (Class aClass)
	throws MobyException {
	try {
	    return (MobyObject)aClass.newInstance();
	} catch (Exception e) {
	    throw new MobyException ("Cannot instantiate data type '" + aClass.getName() + "'.");
	}
    }

    /**************************************************************************
     *
     *************************************************************************/
    public void doEverything() {
	try {
	    if (! parseCmdLine())
		return;
	    mapDataTypes = MobyParser.loadB2JMapping();
	    process (howManyJobs);
	} catch (MobyException e) {
	    reportError (e);
	}
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected void fillObject (MobyObject data)
	throws MobyException {
	createChildren (data);
	setIdAndNamespace (data);
    }

    /**************************************************************************
     * So it can be overridden...
     *************************************************************************/
    protected void setIdAndNamespace (MobyObject data) {
	if (isEmpty (data.getId()))
	    data.setId (shift (ids));
	if (isEmpty (data.getNamespace()))
	    data.setNamespace (shift (ns));
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected boolean createChildren (MobyObject mobj)
	throws MobyException {
	boolean someChildrenCreated = false;
	try {
	    HashMap methodsAndArticleNames = new HashMap();
	    HashMap methodsAndChildTypes = new HashMap();
	    HashMap methodNamesAndHowManyTimes = new HashMap();
	    Class myClass = mobj.getClass();
	    Field[] fields = myClass.getFields();
	    for (int i = 0; i < fields.length; i++) {
		String fieldName = fields[i].getName();
		if (fieldName.startsWith ("ARTICLE_NAME_")) {
		    String artName = (String)fields[i].get (null);
		    String methodPattern = "set_" +  Utils.mobyEscape (Utils.javaEscape (artName));
		    Method[] methods = myClass.getMethods();
		    for (int j = 0; j < methods.length; j++) {
			String methodName = methods[j].getName();
			if (methodName.equals (methodPattern)) {
			    Class[] paramTypes = methods[j].getParameterTypes();
			    Class childType = paramTypes[0];
			    if (childType.equals (String.class)) continue;
			    else if (childType.equals (boolean.class)) continue;
			    else if (childType.isArray()) {
				methodNamesAndHowManyTimes.put (methodName, new Integer (howManyHAS));
			    } else {
				methodsAndArticleNames.put (methods[j], artName);
				methodsAndChildTypes.put (methods[j], childType);
				if (! methodNamesAndHowManyTimes.containsKey (methodName))
				    methodNamesAndHowManyTimes.put (methodName, new Integer (1));
			    }
			}
		    }
		}
	    }
	    if (methodsAndChildTypes.size() > 0) {
		for (Iterator it = methodsAndChildTypes.entrySet().iterator(); it.hasNext(); ) {
		    Map.Entry entry = (Map.Entry)it.next();
		    Method method = (Method)entry.getKey();
		    Class childType = (Class)entry.getValue();
		    int count = ( ((Integer)methodNamesAndHowManyTimes.get (method.getName())).intValue() );
		    while (count-- > 0) {
			if (createOneChild (mobj, method, childType, (String)methodsAndArticleNames.get (method)))
			    someChildrenCreated = true;
		    }
		}
	    } else {
		// this is a leaf that does not have any children...
		if (mobj.isPrimitiveType()) {
		    // ...if it is a primitive type, we may ignore it if there is no more values
		    return setPrimitiveValue (mobj, articleName);
		} else {
		    // ...if it is an Object leaf, we always keep it
		    // but id and namespaces are here like values for
		    // primitives so filltem
		    setIdAndNamespace (mobj);
		    return true;
		}
	    }

	} catch (Exception e) {
	    throw new MobyException ("Error by populating MobyObject: " +
				     e.toString() + "\n" +
				     DGUtils.stackTraceToString (e));

	}
	return someChildrenCreated;
    }

    /**************************************************************************
     *
     *************************************************************************/
    boolean createOneChild (MobyObject parent, Method setMethod, Class childType, String artName)
	throws MobyException {
	try {
	    MobyObject child = createInstance (childType);
	    if (child.isPrimitiveType()) {
		if (setPrimitiveValue (child, artName)) {
		    setMethod.invoke (parent, new Object[] { child });
		    return true;
		}
		return false;  // no more values available
	    } else {
		if (createChildren (child)) {
		    setMethod.invoke (parent, new Object[] { child });
		    return true;
		}
		return false;
	    }
	} catch (Exception e) {
	    e.printStackTrace();
	    throw new MobyException ("Error by setting value: " +
				     e.toString() + "\n" +
				     DGUtils.stackTraceToString (e));
	}
    }

    /**************************************************************************
     *
     *************************************************************************/
    boolean setPrimitiveValue (MobyObject obj, String articleName)
	throws MobyException {
	// find a value for this child - first by article name
	String value = null;
	Vector anValues = (Vector)namedValues.get (articleName);
	if (anValues == null)
	    value = shift (values);
	else
	    value = shift (anValues);
	if (notEmpty (value)) {
	    if (value.startsWith (":") && value.length() > 1) {
		// the value can be actually a file name
		value = FileUtils.getFile (value.substring (1));
		if (isEmpty (value))
		    throw new MobyException ("[" + value + "] An empty or not reachable file.");
	    }
	    obj.setValue (value);
	    return true;
	} else {
	    return false;  // no more values available
	}
    }

    /**************************************************************************
     *
     *************************************************************************/
    public boolean fillRequest (MobyPackage mobyInput, int jobCount)
	throws MobyException {

	// first go to the usual iteration (to fill 'mobyInput')
	if (! super.fillRequest (mobyInput, jobCount))
	    return false;

	// ...then intercept to allow printing the whole input
	if (showInputAsString)
	    System.out.println (mobyInput.toString());

	return true;
    }

    /**************************************************************************
     *
     *************************************************************************/
    public String fillRequest()
	throws MobyException {
	if (inputFile == null)
	    return null;
	try {
	    return FileUtils.getFile (inputFile, false);
	} catch (IOException e) {
	    throw new MobyException ("Cannot read input XML file: " + e.toString());
	} catch (GException e) {
	    throw new MobyException (e.getMessage());
	}
    }

    /**************************************************************************
     *
     *************************************************************************/
    public String interceptRequest (String xmlInput)
	throws MobyException {
	if (showInputXML)
	    System.out.println (xmlInput);

	// decide whether to go and do something
	if (localClass == null && isEmpty (serviceName) && ! loop)
	    return null;

	return xmlInput;
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected String callLocalService (String xmlInput)
	throws MobyException {

	if (isEmpty (serviceName))
	    throw new MobyException
		("Service name was not given. Try to add parameter: -service <name>");

	String methodName = serviceName;
	Method method = null;
	Object service = null;
	try {
	    service = ICreator.createInstance (localClass);
	    method = localClass.getMethod (methodName,
					   new Class[] { Object.class });
	    return filterMobyResponseType
		(method.invoke (service, new Object[] { xmlInput }));

	} catch (NoSuchMethodException e) {
	    StringBuffer buf = new StringBuffer();
	    buf.append ("Method '");
	    buf.append (methodName);
	    buf.append ("' was not found");
	    if (service != null) {
		buf.append (" in the object ");
		buf.append (service.getClass().getName());
		buf.append (".\nThe object has only following public methods:\n");
		Method[] methods = service.getClass().getMethods();
		for (int i = 0; i < methods.length; i++) {
		    buf.append ("\t");
		    buf.append (methods[i]);
		    buf.append ("\n");
		}
	    }
	    throw new MobyException (new String (buf));
	    
	} catch (GException e) {
	    throw new MobyException (e.getMessage());
	} catch (IllegalAccessException e) {
	    throw new MobyException ("IllegalAccessException: " + e.getMessage());
	} catch (IllegalArgumentException e) {
	    throw new MobyException ("IllegalArgumentException: " + e.getMessage());
	} catch (InvocationTargetException e) {
	    throw new MobyException ("InvocationTargetException: " +
				     e.getTargetException().toString());
	}
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected void showOrStore (String contents)
	throws MobyException {
	if (outputFile == null) {
	    System.out.println (contents);
	} else {
	    try {
		Utils.createFile (new File (outputFile), contents);
	    } catch (MobyException e) {
		// catching it here to be able at least to print the
		// result
		System.out.println (contents);
		throw e;
	    }
	}
    }

    /**************************************************************************
     * Call either a local class implementing a service, or pass it to
     * the superclass that does a real (usual) SOAP call to a service
     * (or first to a registry and then to a service). <p>
     *
     * The local class is used if its name was given on the
     * command-line. This is meant to be used to test services before
     * they are deployed in a servlet environment (such as Apache/Axis). <p>
     *
     * @see BaseClient#callRemoteService
     *
     * @throws MobyException if (a) a local class cannot be
     * instantiated, or if (b) a local class returned an unexpected
     * result type (should return only String or array of bytes), or
     * if a (c) super-class call to remote service failed
     *************************************************************************/
    public String callRemoteService (String xmlInput)
	throws MobyException {

	if (loop)
	    return xmlInput;
	else if (localClass != null)
	    return callLocalService (xmlInput);
	else
	    return super.callRemoteService (xmlInput);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public boolean useResponse (String xmlResponse)
	throws MobyException {

	// intercept to allow printing the whole XML response...
	if (resultInXML)
	    showOrStore (xmlResponse);

	// ...then go to the usual iteration over jobs/queries
	return true;
    }

    /**************************************************************************
     *
     *************************************************************************/
    public void useResponse (MobyPackage mobyResponse)
	throws MobyException {

	// intercept only to allow printing the whole response...
	if (resultAsString)
	    showOrStore (mobyResponse.toString());

	// ...then go to the usual iteration over jobs/queries
	super.useResponse (mobyResponse);
    }

    //
    // These were abstract methods
    //

    /**************************************************************************
     *
     *************************************************************************/
    public MobyServiceLocator getServiceLocator()
	throws MobyException {

	MobyServiceLocator locator = new MobyServiceLocator();
	MobyService service = new MobyService (serviceName);
	service.setAuthority (serviceAuthority);
	service.setURL (serviceEndpoint);
	locator.setService (service);
	locator.setRegistryEndpoint (registryEndpoint);
	locator.setRegistryNamespace (registryURI);
	locator.setAsBytes (sendAsBytes);
	return locator;
    }



    /**************************************************************************
     *
     *************************************************************************/
    public boolean fillRequest (MobyJob request, MobyPackage inputContext)
	throws MobyException {

	// create an empty data object first
	MobyObject data = createInstance (obj);

	if (howManyCols > 0) {
	    // let's use values/ids/namespaces for a Collection
	    Vector v = new Vector();
	    for (int i = 0; i < howManyCols; i++) {
		MobyObject result = createInstance (data);
		fillObject (result);
		if (result.isPrimitiveType() && isEmpty (result.getValue()))
		    break;
		v.addElement (result);
	    }
	    MobyObject[] dataSet = new MobyObject [ v.size() ];
	    v.copyInto (dataSet);
	    request.setDataSet (dataSet, articleName);

	} else {
	    // let's use values/ids/namespaces for a Simple
	    fillObject (data);
	    if ( (! data.isPrimitiveType()) || notEmpty (data.getValue()))
		request.setData (data, articleName);
	    else
		return false;
	}

	// add secondary parameters
	for (Iterator it = secondaries.keySet().iterator(); it.hasNext(); ) {
	    String secName = (String)it.next();
	    Vector secValues = (Vector)secondaries.get (secName);
	    String secValue = shift (secValues);
	    request.setParameter (secName, secValue);
	}

	return true;
    }

    /**************************************************************************
     *
     *************************************************************************/
    public boolean useResponse (MobyJob response,
				MobyPackage responseContext)
	throws MobyException {
	return true;
    }

    /**************************************************************************
     *
     *************************************************************************/
    public static void main (String [] args) {
	new BaseCmdLineClient (args).doEverything();
    }

}
