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

package org.biomoby.shared.parser;

import org.biomoby.shared.Utils;
import org.biomoby.shared.MobyException;
import org.biomoby.shared.datatypes.MobyObject;
import org.biomoby.shared.datatypes.MapPrimitiveDataTypes;
import org.biomoby.shared.datatypes.MapDataTypesIfc;

import org.jdom.Element;

import java.util.Vector;
import java.util.Enumeration;

/**
 * This class represents one BioMoby query (in a client request), or a
 * result of one query (in a service response). There can be more
 * queries (jobs) in one network request to a BioMoby service. If a
 * network request contains more jobs, also the corresponding service
 * response must contain the same number of jobs. <p>
 *
 * Each job is identified (within a service request or response) by
 * its <tt>id</tt> (methods {@link #getId} and {@link #setId}, or a
 * constructor {@link #MobyJob}). <p>
 *
 * The main contents of each job are {@link MobyDataElement data
 * elements}. They can represent either a {@link MobySimple Simple}
 * element, a {@link MobyCollection Collection}, or a {@link
 * MobyParameter Parameter}. The class has many methods to find and
 * get these data elements. The best is to find them by name but
 * unfortunately not all of them have always a name. Therefore, some
 * methods of this class try to apply some {@link #getData(String)
 * heuristics to locate} the most probable data lement. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: MobyJob.java,v 1.7 2006/05/09 13:32:09 senger Exp $
 */

public class MobyJob {

    protected String id = "";  // queryID
    protected Vector dataElements = new Vector();  // of type MobyDataElement

    /**************************************************************************
     * Default constructor.
     *************************************************************************/
    public MobyJob() {
    }

    /**************************************************************************
     * Another constructor, naming this job by an <tt>id</tt>.
     *************************************************************************/
    public MobyJob (String id) {
	setId (id);
    }

    /**************************************************************************
     *
     *************************************************************************/
    private static String MSG_OUT_OF_BOUNDS (int index, int size) {
	return ("Asked for a data element '" + index +
		"' while there is only '" + size +
		"' elements available.");
    }

//    /**************************************************************************
//     *
//     *************************************************************************/
//    private String MSG_NAME_NOT_FOUND (String name) {
//	return ("No data element '" + name + "' found in job '" + id + "'.");
//    }
//
//    /**************************************************************************
//     *
//     *************************************************************************/
//    private String MSG_TYPE_NOT_FOUND (String name, String dataTypeName) {
//	return ("No data element of type '" + dataTypeName +
//		"' found in job '" + id +
//		"'. And I was looking both for named element '" + name +
//		"' and for unnamed elements.");
//    }
//
    /**************************************************************************
     *
     *************************************************************************/
    static protected boolean notEmpty (String value) {
	return ( value != null && ! "".equals (value.trim()) );
    }
    static protected boolean isEmpty (String value) {
	return ! notEmpty (value);
    }

    /**************************************************************************
     * Extend space for data elements if necessary...
     *************************************************************************/
    protected void insertDataElement (MobyDataElement element, int index)
	throws MobyException {

	// extend our data space if necessary
	if (index < 0) index = 0;
	while (index > dataElements.size())
	    dataElements.addElement (new MobySimple());

	// put it on a right place
	dataElements.add (index, element);
    }

    /**************************************************************************
     * Return true if the data element 'mde' contains a non-empty data
     * object that corresponds with the given 'dataTypeName'. It
     * corresponds if a non-empty data object is the same type as
     * 'dataTypeName', or it is its subclass. <p>
     *
     * Note that 'mde' can be a collection - in that case use its
     * first element for finding what data type it contains. <p>
     *
     * Also allow both Java (String) and BioMoby (MobyString) names
     * for primitive types in 'dataTypeName.
     *************************************************************************/
    protected boolean matchDataType (MobyDataElement mde, String dataTypeName)
	throws MobyException {

	if (mde instanceof MobySimple) {
	    return matchDataTypeOfSimple ((MobySimple)mde, dataTypeName);
	} else if (mde instanceof MobyCollection) {
	    MobySimple[] simples = ((MobyCollection)mde).getData();
	    if (simples == null || simples.length == 0)
		return false;
	    return matchDataTypeOfSimple (simples[0], dataTypeName);
	}
	return false;
    }

    MapDataTypesIfc mapDataTypes;
    protected boolean matchDataTypeOfSimple (MobySimple simple, String dataTypeName)
	throws MobyException {

	MobyObject data = simple.getData();
	if (data == null) return false;

	// this is fastest - and often enough: it tests matching for
	// equality
	String dataElementClassName = data.getClass().getName();
	if (Utils.simpleClassName (dataElementClassName).equals (dataTypeName))
	    return true;

	// if above fails, we need to test whether 'data' is actually a
	// subclass of 'dataTypeName'
	Class dataElementClass = data.getClass();
	if (mapDataTypes == null)
	    mapDataTypes = MobyParser.loadB2JMapping();
	Class parentClass = mapDataTypes.getClass (dataTypeName);
	if ( parentClass != null &&
	     parentClass.isAssignableFrom (dataElementClass) )
	    return true;

	// finally, check more class names for primitive types
	String primitiveDataTypeName =
	    MapPrimitiveDataTypes.getClassNameForPrimitives (dataTypeName);
	if (primitiveDataTypeName == null) return false;
	if (Utils.simpleClassName (primitiveDataTypeName)
	    .equals (dataElementClassName))
	    return true;
	return false;
    }

    /**************************************************************************
     * Find and return the 'index'-th data element, assuming that it
     * is a Simple data element. The index starts from zero, and both
     * Simples and Collections elements are counted together. <p>
     *
     * Note that the BioMoby API does not dictate any order on the
     * data elements. Therefore, this method is not public, and it is
     * used from all other getData methods only. <p>
     *
     * @param index of the data element to be returned (a negative
     * index is changed to zero)
     * @throws MobyException if the index-th data element is not a
     * Simple, or if the index is too large
     *************************************************************************/
    protected MobyObject getData (int index)
	throws MobyException {

	if (index < 0) index = 0;
	try {
	    MobyDataElement element = (MobyDataElement)dataElements.elementAt (index);
	    if (element instanceof MobySimple)
		return ((MobySimple)element).getData();
	    else if (element instanceof MobyCollection)
		throw new MobyException ("Data element on index " + index +
					 " is not a Simple object but a Collection.");
	    else
		throw new MobyException ("Data element on index " + index +
					 " is not a Simple object but a '" + element.getClass().getName() +
					 "'.");
	} catch (ArrayIndexOutOfBoundsException e) {
	    throw new MobyException (MSG_OUT_OF_BOUNDS (index, dataElements.size()));
	}
    }

    /**************************************************************************
     * Find and return the 'index'-th data element, assuming that it
     * is a Collection data element. The index starts from zero, and both
     * Simples and Collections elements are counted together. <p>
     *
     * Note that the BioMoby API does not dictate any order on the
     * data elements. Therefore, this method is not public, and it is
     * used from all other getData methods only. <p>
     *
     * @param index of the data element to be returned (a negative
     * index is changed to zero)
     * @throws MobyException if the index-th data element is not a
     * Collection, or if the index is too large
     *
     *************************************************************************/
    public MobyObject[] getDataSet (int index)
	throws MobyException {

	if (index < 0) index = 0;
	try {
	    MobyDataElement element = (MobyDataElement)dataElements.elementAt (index);
	    if (element instanceof MobyCollection) {
		MobySimple[] simples = ((MobyCollection)element).getData();
		MobyObject[] data = new MobyObject [simples.length];
		for (int i = 0; i < simples.length; i++)
		    data[i] = simples[i].getData();
		return data;

	    } else if (element instanceof MobySimple)
		throw new MobyException ("Data element on index " + index +
					 " is not a Collection but a Simple object.");
	    else
		throw new MobyException ("Data element on index " + index +
					 " is not a Collection but a '" + element.getClass().getName() +
					 "'.");
	} catch (ArrayIndexOutOfBoundsException e) {
	    throw new MobyException (MSG_OUT_OF_BOUNDS (index, dataElements.size()));
	}
    }


    //
    // public methods
    //

    /**************************************************************************
     * Format all non-empty public members in a human-readable way.
     *
     * @see #format for making the returned string better indented
     * @return a formatted string
     *************************************************************************/
    public String toString() {
	StringBuffer buf = new StringBuffer();
	buf.append ("Query ID: " + id + "\n");
	buf.append ("Data elements:\n");
	for (Enumeration en = dataElements.elements(); en.hasMoreElements(); ) {
	    buf.append (((MobyDataElement)en.nextElement()).format (1));
	}
	return new String (buf);
    }

    /**************************************************************************
     * Return the same contents as {@link #toString} method but
     * indented by level expressed in the parameter 'indent'. It is
     * useful when a hierarchy of objects call <tt>toString</tt>
     * methods on their children/members. For example, in order to
     * list all data elements in a MobyJob object, the method
     * <tt>toString</tt> in <tt>MobyJob</tt> includes code like this:
     *
     *<pre>
     *  StringBuffer buf = new StringBuffer();
     *  for (Enumeration en = dataElements.elements(); en.hasMoreElements(); )
     *    buf.append (((MobyDataElement)en.nextElement()).format (1));
     *</pre>
     *
     * @param indent means a level of wanted indentation: number 1
     * means three spaces, number two six spaces, etc.
     * @return a formatted, and indented, string
     *************************************************************************/
    public String format (int indent) {
	return Utils.format (this, indent);
    }

    /**************************************************************************
     * Create an XML element representing this object. <p>
     *
     * @return a <a
     * href="http://www.jdom.org/docs/apidocs/org/jdom/Element.html">jDom
     * element</a> that can be easily incorporated into bigger a XML
     * document
     *************************************************************************/
    public Element toXML() {
	Element elem = MobyPackage.getXMLElement (MobyTags.MOBYDATA);
	MobyPackage.setXMLAttribute (elem, MobyTags.QUERYID, getId());
	for (Enumeration en = dataElements.elements(); en.hasMoreElements(); )
	    elem.addContent ( ((MobyDataElement)en.nextElement()).toXML() );

	return elem;
    }

    /**************************************************************************
     * Create an XML element representing this object. <p>
     *
     * @return a <a
     * href="http://www.jdom.org/docs/apidocs/org/jdom/Element.html">jDom
     * element</a> that can be easily incorporated into bigger a XML
     * document
     *************************************************************************/
    public static Element toXML (String jobId, Element[] data) {
	Element elem = MobyPackage.getXMLElement (MobyTags.MOBYDATA);
	MobyPackage.setXMLAttribute (elem, MobyTags.QUERYID, jobId);
	for (int i = 0; i < data.length; i++)
	    elem.addContent (data[i]);
	return elem;
    }

    /**************************************************************************
     * Set a "query id" that identifies this job uniquely within a
     * client request or a service response. <p>
     *
     * @param id to be stored. If it is null, an empty string is
     * stored instead.
     *************************************************************************/
    public void setId (String id) {
	this.id = (id == null ? "" : id);
    }

    /**************************************************************************
     * @see #setId
     *************************************************************************/
    public String getId() {
	return id;
    }

    /**************************************************************************
     * Return a number of data elements contained in this job.
     *************************************************************************/
    public int size() {
	return dataElements.size();
    }

    /**************************************************************************
     * Replace all current data elements (if any) by the new ones.
     *************************************************************************/
    public void setDataElements (MobyDataElement[] dataElements) {
	this.dataElements = new Vector();
	for (int i = 0; i < dataElements.length; i++)
	    this.dataElements.addElement (dataElements[i]);
    }

    /**************************************************************************
     * Add 'dataElement' to this job.
     *************************************************************************/
    public void addDataElement (MobyDataElement dataElement) {
	dataElements.addElement (dataElement);
    }

    /**************************************************************************
     * Return all data elements stored in this job. There are also
     * convenient methods to return just some data elements (by name,
     * by index, etc.).
     *************************************************************************/
    public MobyDataElement[] getDataElements() {
	MobyDataElement[] result = new MobyDataElement [ dataElements.size() ];
	dataElements.copyInto (result);
	return result;
    }

    //
    // Convenient methods (for Simples)
    //

    /**************************************************************************
     * Find and return the first data element of type Simple (which
     * may not be the first data element in this job). <p>
     *
     * @return null if no such data element was found
     * @throws MobyException if there was an internal error
     *************************************************************************/
    public MobyObject getData()
	throws MobyException {
	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if (mde instanceof MobySimple)
		return getData (i);
	}
	return null;
    }

    /**************************************************************************
     * Find and return a data element of a Simple type that is named
     * by 'name'. This is how it tries to find the element: <ol>
     *
     * <li> Try to find a Simple with an article name equals to
     * 'name'.
     *
     * <li> If it fails, count how many unnamed Simple data elements
     * are present. If only one, return it. <p>
     *
     * <li Return null. <p>
     *
     * </ol>
     *
     * @param name is an article name we are looking for
     *
     * @return found data element or null
     *
     * @throws MobyException if there was an internal error
     *************************************************************************/
    public MobyObject getData (String name)
	throws MobyException {

	// try article names
	int countOfUnnamed = 0;
	int indexOfLastUnnamed = -1;
	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if (mde instanceof MobySimple) {
		String mdeName = mde.getName();
		if (mdeName.equals (name)) return getData (i);
		if (isEmpty (mdeName)) {
		    countOfUnnamed++;
		    indexOfLastUnnamed = i;
		}
	    }
	}

	// more tha one unnamed?
	if (countOfUnnamed == 1)
	    return getData (indexOfLastUnnamed);

	// no luck
	return null;
    }

    /**************************************************************************
     * Find and return a data element of a Simple type that is named
     * by 'name' and has data type 'dataTypeName'. This is how it
     * tries to find the element: <ol>
     *
     * <li> Try to find a Simple element matching both the article
     * name ('name') and a data type ('dataTypeName'). <p>
     *
     * <li> If it fails, try to find an unnamed Simple data element
     * matching just the data type. <p>
     *
     * <li> Return null. <p>
     * 
     * </ol>
     *
     * For example: If a service expects two inputs, one
     * <tt>String</tt> and one <tt>Integer</tt>, but any of them, or
     * both actually, can be missing, use
     *
     * <pre>getData ("", "String")</pre>
     *
     * and
     *
     * <pre>getData ("", "Integer")</pre>
     *
     * and test results for null values. We use en ampty article names
     * here because there cannot be any confusion (there are no more
     * String or Integer types inputs). However, if the same service
     * was registered with article names, it is better to use them
     * (and if a client does not use them the data will be found
     * anyway). <p>
     *
     * @param name is an article name we are looking for
     * @param dataTypeName is a simple (unqualified) class name. For
     * example, for a data type represented by a Java class
     * <tt>org.biomoby.shared.datatypes.GenericSequence</tt> put here
     * just <tt>GenericSequence</tt>. For the BioMoby primitive types,
     * put them either their simple, unqualified class name (such as
     * <tt>MobyString</tt>), or their native BioMoby name (such as
     * <tt>String</tt>.
     *
     * @return found data element or null
     *
     * @throws MobyException if there was an internal error
     *************************************************************************/
    public MobyObject getData (String name, String dataTypeName)
	throws MobyException {

	// try to match both: article name and data type name
	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if ( (mde instanceof MobySimple) &&
		 mde.getName().equals (name) &&
		 matchDataType (mde, dataTypeName) )
		return getData (i);
	}

	// try to match only data type name where article name is empty
	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if ( (mde instanceof MobySimple) &&
		 ( isEmpty (mde.getName()) || isEmpty (name) ) &&
		  matchDataType (mde, dataTypeName) )
		return getData (i);
	}

	// no luck
	return null;
    }

    /**************************************************************************
     * Create an un-named Simple object as the first data element.
     *************************************************************************/
    public void setData (MobyObject data)
	throws MobyException {
	setData (data, "", 0);
    }

    /**************************************************************************
     * Create a named Simple object as the first data element.
     *************************************************************************/
    public void setData (MobyObject data, String name)
	throws MobyException {
	setData (data, name, 0);
    }

    /**************************************************************************
     * Create a named Simple object and put it as the 'index'-th data
     * element. Expand data elements by empty Simple objects if the
     * index is out of bounds.
     *************************************************************************/
    public void setData (MobyObject data, String name, int index)
	throws MobyException {

	// create and fill an output data container
	MobySimple simple = new MobySimple();
	simple.setData (data);
	simple.setName (name);

	// extend our data space if necessary and put it on a right place
	insertDataElement (simple, index);
    }

    /**************************************************************************
     * Create an un-named Simple object and add it as the last one in
     * the current data elements.
     *************************************************************************/
    public void addData (MobyObject data)
	throws MobyException {
	setData (data, "", dataElements.size());
    }

    /**************************************************************************
     * Create a named Simple object and add it as the last one in the
     * current data elements.
     *************************************************************************/
    public void addData (MobyObject data, String name)
	throws MobyException {
	setData (data, name, dataElements.size());
    }


    //
    // Convenient methods (for Collections)
    //

    /**************************************************************************
     * Find and return the first data element of type Collection
     * (which may not be the first data element in this job). <p>
     *
     * @return null if no such data element was found
     * @throws MobyException if there was an internal error
     *************************************************************************/
    public MobyObject[] getDataSet()
	throws MobyException {
	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if (mde instanceof MobyCollection)
		return getDataSet (i);
	}
	return null;
    }

    /**************************************************************************
     * Find and return a data element of a Collection type that is named
     * by 'name'. This is how it tries to find the element: <ol>
     *
     * <li> Try to find a Collection with an article name equals to
     * 'name'.
     *
     * <li> If it fails, count how many unnamed Collection data elements
     * are present. If only one, return it. <p>
     *
     * <li Return null. <p>
     *
     * </ol>
     *
     * @param name is an article name we are looking for
     *
     * @return found data element or null
     *
     * @throws MobyException if there was an internal error
     *************************************************************************/
    public MobyObject[] getDataSet (String name)
	throws MobyException {

	// try article names
	int countOfUnnamed = 0;
	int indexOfLastUnnamed = -1;
	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if (mde instanceof MobyCollection) {
		String mdeName = mde.getName();
		if (mdeName.equals (name)) return getDataSet (i);
		if (isEmpty (mdeName)) {
		    countOfUnnamed++;
		    indexOfLastUnnamed = i;
		}
	    }
	}

	// more tha one unnamed?
	if (countOfUnnamed == 1)
	    return getDataSet (indexOfLastUnnamed);

	// no luck
	return null;
    }

    /**************************************************************************
     * Find and return a data element coming from a Collection type
     * that is named by 'name' and has data type 'dataTypeName'. This
     * is how it tries to find the element: <ol>
     *
     * <li> Try to find a collection element matching both the article
     * name ('name') and a data type ('dataTypeName'). Matching means
     * that the first element of this collection matches. <p>
     *
     * <li> If it fails, try to find an unnamed Collection data
     * element matching just the data type. Again, matching is done
     * for an element of this collection. <p>
     *
     * <li> If it fails, and if here given 'name' is empty, try to
     * find any Collection data element matching the data type. (<em>I
     * am not sure that this rule is what we want...</em>)<p>
     *
     * <li> Return null. <p>
     * 
     * </ol>
     *
     * @param name is an article name we are looking for
     * @param dataTypeName is a simple (unqualified) class name. For
     * example, for a data type represented by a Java class
     * <tt>org.biomoby.shared.datatypes.GenericSequence</tt> put here
     * just <tt>GenericSequence</tt>. For the BioMoby primitive types,
     * put them either their simple, unqualified class name (such as
     * <tt>MobyString</tt>), or their native BioMoby name (such as
     * <tt>String</tt>.
     *
     * @return found data element or null
     *
     * @throws MobyException if there was an internal error
     *************************************************************************/
    public MobyObject[] getDataSet (String name, String dataTypeName)
	throws MobyException {

	// try to match both: article name and data type name
	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if ( (mde instanceof MobyCollection) &&
		 mde.getName().equals (name) &&
		 matchDataType (mde, dataTypeName) )
		return getDataSet (i);
	}

	// try to match only data type name where article name is empty
	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if ( (mde instanceof MobyCollection) &&
		 ( isEmpty (mde.getName()) || isEmpty (name) ) &&
		 matchDataType (mde, dataTypeName) )
		return getDataSet (i);
	}

	// no luck
	return null;
    }

    /**************************************************************************
     * Create an un-named Collection object as the first data element.
     *************************************************************************/
    public void setDataSet (MobyObject[] data)
	throws MobyException {
	setDataSet (data, "", 0);
    }

    /**************************************************************************
     * Create a named Collection object as the first data element.
     *************************************************************************/
    public void setDataSet (MobyObject[] data, String name)
	throws MobyException {
	setDataSet (data, name, 0);
    }

    /**************************************************************************
     * Create a named Collection object and put it as the 'index'-th data
     * element. Expand data elements by empty Simple objects if the
     * index is out of bounds.
     *
     * The individual elements of the collection get (article) names
     * from the individual 'data' objects.
     *************************************************************************/
    public void setDataSet (MobyObject[] data, String name, int index)
	throws MobyException {

	// create and fill an output data container
	MobyCollection collection = new MobyCollection();
	for (int i = 0; i < data.length; i++) {
	    MobySimple simple = new MobySimple();
	    simple.setData (data[i]);
	    simple.setName (data[i].getName());
	    collection.addData (simple);
	}
	collection.setName (name);

	// extend our data space if necessary and put it on a right place
	insertDataElement (collection, index);
    }

    /**************************************************************************
     * Create an un-named Collection object and add it as the last one
     * in the current data elements.
     *************************************************************************/
    public void addDataSet (MobyObject[] data)
	throws MobyException {
	setDataSet (data, "", dataElements.size());
    }

    /**************************************************************************
     * Create a named Collection object and add it as the last one in
     * the current data elements.
     *************************************************************************/
    public void addDataSet (MobyObject[] data, String name)
	throws MobyException {
	setDataSet (data, name, dataElements.size());
    }

    //
    // Convenient methods (for Parameter)
    //

    /**************************************************************************
     * Find a Parameter object named by 'name', and return its
     * value. Note that all Parameters should have name. <p>
     *
     * @param name is an article name we are looking for
     * @return found parameter, or null
     *
     * @throws MobyException if 'name' represents a different type of
     * data element (not a parameter)
     *************************************************************************/
    public String getParameter (String name)
	throws MobyException {

	for (int i = 0; i < dataElements.size(); i++) {
	    MobyDataElement mde = (MobyDataElement)dataElements.elementAt (i);
	    if (mde.getName().equals (name)) {
		if (mde instanceof MobyParameter)
		    return ((MobyParameter)mde).getValue();
		else if (mde instanceof MobySimple)
		    throw new MobyException ("Data element named '" + name +
					     "' is not a Parameter but a Simple object.");
		else if (mde instanceof MobyCollection)
		    throw new MobyException ("Data element named '" + name +
					     "' is not a Parameter but a Collection.");
		else
		    throw new MobyException ("Data element named '" + name +
					     "' is not a Parameter but a '" +
					     mde.getClass().getName() + "'.");
	    }
	}
	return null;
    }

    /**************************************************************************
     * Set secondary parameter 'name' to 'value'. <p>
     *
     * @param name of a secondary paraneter to be set
     * @param value to be set
     *************************************************************************/
    public void setParameter (String name, String value) {
	MobyParameter parameter = new MobyParameter();
	parameter.setName (name);
	parameter.setValue (value);
	addDataElement (parameter);
    }

}
