// MobyPackage.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.MobyException;
import org.biomoby.shared.Utils;
import org.biomoby.shared.datatypes.MobyObject;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.CDATA;
import org.jdom.Namespace;
import org.jdom.output.XMLOutputter;
import org.jdom.output.Format;

import java.util.Map;
import java.util.Vector;
import java.util.Enumeration;
import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * This is the main container for Biomoby data once they are extracted
 * from an XML format. It represents the package either sent from a
 * client to a Biomoby service, or vice-versa. It also includes
 * methods for parsing data from XML and for creating data back into
 * XML. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: MobyPackage.java,v 1.8 2008/02/28 05:21:48 senger Exp $
 */

public class MobyPackage {

    private static org.apache.commons.logging.Log log =
	org.apache.commons.logging.LogFactory.getLog (MobyPackage.class);

    protected String authority;
    protected String serviceNotes;
    protected boolean isNotesCDATA = false;
    protected Vector errors = new Vector();  // of type ServiceException
    protected Vector jobs = new Vector();    // of type MobyJob

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

    /**************************************************************************
     * Constructing a simplest MobyPackage - with one MobyObject. <p>
     *
     * @param mObj to be inserted in this package as a simple object
     * in a single job
     * @param articleName is the name given to the only Simple
     *************************************************************************/
    public MobyPackage (MobyObject mObj, String articleName) {
	try {
	    MobyJob job = new MobyJob();
	    job.addData (mObj, articleName);
	    addJob (job);
	} catch (MobyException e) {
	    log.error ("A strange error: " + e.getMessage());
	}
    }

    /**************************************************************************
     * Constructing a MobyPackage object from XML. The input XML can
     * be given as a String, byte[], or a File.
     *************************************************************************/
    public static MobyPackage createFromXML (Object xmlData)
	throws MobyException {
	return createFromXML (xmlData, null, null);
    }

    /**************************************************************************
     * Constructing a MobyPackage object from XML. The input XML can
     * be given as a String, byte[], or a File. <p>
     *
     * Additionally, it passes to the XML parser the
     * 'lowestKnownDataType' as a falback object (the role of a
     * fallback object is explained in {@link MobyParser}.
     *************************************************************************/
    public static MobyPackage createFromXML (Object xmlData,
					     String lowestKnownDataType)
	throws MobyException {
	return createFromXML (xmlData, lowestKnownDataType, null);
    }

    /**************************************************************************
     * Constructing a MobyPackage object from XML. The input XML can
     * be given as a String, byte[], or a File. <p>
     *
     * Additionally, it passes to the XML parser the
     * 'lowestKnownDataType' as a falback object (the role of a
     * fallback object is explained in {@link MobyParser}.
     *************************************************************************/
    public static MobyPackage createFromXML (Object xmlData,
					     Map<String,String> lowestKnownDataTypes)
	throws MobyException {
	return createFromXML (xmlData, null, lowestKnownDataTypes);
    }

    /**************************************************************************
     * Constructing a MobyPackage object from XML. The input XML can
     * be given as a String, byte[], or a File. <p>
     *
     * Additionally, it passes to the XML parser the
     * 'lowestKnownDataType' as a falback object (the role of a
     * fallback object is explained in {@link MobyParser}.
     *************************************************************************/
    protected static MobyPackage createFromXML (Object xmlData,
						String lowestKnownDataType,
						Map<String,String> lowestKnownDataTypes)
	throws MobyException {
	
	MobyParser parser = null;
	if (lowestKnownDataTypes != null && lowestKnownDataTypes.size() > 0) {
	    parser = new MobyParser (lowestKnownDataTypes);
	} else {
	    parser = new MobyParser (lowestKnownDataType);
	}

	if (xmlData instanceof byte[]) {
	    return parser.parse ( new ByteArrayInputStream ((byte[])xmlData) );
	} else if (xmlData instanceof File) {
	    try {
		return  parser.parse ( new FileInputStream ((File)xmlData) );
	    } catch (IOException e) {
		throw new MobyException (e.toString());
	    }
	} else if (xmlData instanceof String) {
	    return  parser.parse ( new StringReader ((String)xmlData) );
	} else
	    throw new MobyException
		("The Biomoby data should be sent/received either as type String or base64/byte[]. But they are of type '" +
		 xmlData.getClass().getName() + "'.");
    }

    /**************************************************************************
     * 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();
	if (authority != null) buf.append ("Authority: " + authority + "\n");
	if (serviceNotes != null) buf.append ("Service notes: " + serviceNotes + "\n");
	if (errors.size() > 0) {
	    buf.append ("Exceptions:\n");
	    int count = 1;
	    for (Enumeration en = errors.elements(); en.hasMoreElements(); ) {
		buf.append ("(" + count++ + ") ");
		buf.append (((ServiceException)en.nextElement()).format (1));
		buf.append ("\n");
	    }
	}
	buf.append ("Jobs (invocations):\n");
	int count = 1;
	for (Enumeration en = jobs.elements(); en.hasMoreElements(); ) {
	    buf.append ("(" + count++ + ") ");
	    buf.append (((MobyJob)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. <p>
     *
     * @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);
    }

    /**************************************************************************
     * Return a jDOM XML element equipped with the BioMoby
     * namespace. This is just a convenient utility method. <p>
     *
     * @param name of the returned element
     * @return a jDOM element
     *************************************************************************/
    public static Element getXMLElement (String name) {
	return new Element (name,
			    Namespace.getNamespace (MobyTags.MOBY_XML_NS_PREFIX,
						    MobyTags.MOBY_XML_NS));
    }

    /**************************************************************************
     * Set attribute 'name' to 'value' in a jDOM XML element, equipped
     * with the BioMoby namespace. This is just a convenient utility
     * method. <p>
     *
     * Do it only if 'value' is not empty. If you need to create an
     * attribute even if the value is empty use instead method {@link
     * #setXMLAttributeForced}. <p>
     *
     * @param element where a new attribute is created
     * @param name of the created attribute
     * @param value of the created attribute
     *************************************************************************/
    public static void setXMLAttribute (Element element,
					String name, String value) {
	if (value != null && ! "".equals (value.trim()))
	    element.setAttribute (name, value,
				  Namespace.getNamespace (MobyTags.MOBY_XML_NS_PREFIX,
							  MobyTags.MOBY_XML_NS));
    }

    /**************************************************************************
     * Set attribute 'name' to 'value' in a jDOM XML element, equipped
     * with the BioMoby namespace. This is just a convenient utility
     * method. <p>
     *
     * Do it even for an empty 'value'. <p>
     *
     * @param element where a new attribute is created
     * @param name of the created attribute
     * @param value of the created attribute
     *************************************************************************/
    public static void setXMLAttributeForced (Element element,
					      String name, String value) {
	if (value == null) value = "";
	element.setAttribute (name, value,
			      Namespace.getNamespace (MobyTags.MOBY_XML_NS_PREFIX,
						      MobyTags.MOBY_XML_NS));
    }

    /**************************************************************************
     * Convert the whole contents and all its children to a Biomoby
     * compliant XML string. <p>
     *************************************************************************/
    public String toXML() {
	Document doc = toXMLDocument();

	// Generate the textual version of the document
	XMLOutputter xo = new XMLOutputter();
	xo.setFormat(Format.getPrettyFormat());
	return xo.outputString (doc);
    }

    /**************************************************************************
     * Convert the whole contents and all its children to a Biomoby
     * compliant XML document. <p>
     *************************************************************************/
    public Document toXMLDocument() {
	Element root = getXMLElement (MobyTags.MOBY);
	Document doc = new Document (root);

	Element elemContent = getXMLElement (MobyTags.MOBYCONTENT);
	setXMLAttribute (elemContent, MobyTags.AUTHORITY, authority);

	if (serviceNotes != null || errors.size() > 0) {
	    Element sNotes = getXMLElement (MobyTags.SERVICENOTES);
	    for (Enumeration en = errors.elements(); en.hasMoreElements(); )
		sNotes.addContent ( ((ServiceException)en.nextElement()).toXML() );
	    if (serviceNotes != null) {
		Element notes = getXMLElement (MobyTags.NOTES);
		if (isNotesCDATA)
		    notes.addContent (new CDATA (serviceNotes));
		else
		    notes.setText (serviceNotes);
		sNotes.addContent (notes);
	    }
	    elemContent.addContent (sNotes);
	}

	for (Enumeration en = jobs.elements(); en.hasMoreElements(); )
	    elemContent.addContent ( ((MobyJob)en.nextElement()).toXML() );

	root.addContent (elemContent);
	return doc;
    }

    /**************************************************************************
     * A static alternative returning much simpler XML document -
     * without authority, without service notes, just with the given
     * jobs. And the jobs are given already as DOM Elements.
     *************************************************************************/
    public static Document toXMLDocument (Element[] jobs) {
	Element root = getXMLElement (MobyTags.MOBY);
	Document doc = new Document (root);
	Element elemContent = getXMLElement (MobyTags.MOBYCONTENT);
	for (int i = 0; i < jobs.length; i++)
	    elemContent.addContent (jobs[i]);
	root.addContent (elemContent);
	return doc;
    }

    /**************************************************************************
     * Return the number of jobs (<em>queries</em>) contained in this
     * package.
     *************************************************************************/
    public int size() {
	return jobs.size();
    }

    /**************************************************************************
     * Store here all jobs. It removes all previously (if any) stored
     * jobs.
     *************************************************************************/
    public void setJobs (MobyJob[] jobs) {
	this.jobs = new Vector();
	for (int i = 0; i < jobs.length; i++)
	    this.jobs.addElement (jobs[i]);
    }

    /**************************************************************************
     * Add one job to the already stored here.
     *************************************************************************/
    public void addJob (MobyJob job) {
	jobs.addElement (job);
    }

    /**************************************************************************
     * Get back all jobs stored here.
     *************************************************************************/
    public MobyJob[] getJobs() {
	MobyJob[] result = new MobyJob [ jobs.size() ];
	jobs.copyInto (result);
	return result;
    }

    /**************************************************************************
     * Get back a job indicated by its order number. The 'index'
     * starts at zero.
     *************************************************************************/
    public MobyJob getJob (int index)
	throws MobyException {
	try {
	    return (MobyJob)jobs.get (index);
	} catch (ArrayIndexOutOfBoundsException e) {
	    throw new MobyException
		("Asked for a job '" + index +
		 "' while there is only '" + jobs.size() +
		 "' jobs available.");
	}
    }

    /**************************************************************************
     * Fill in an authority of this whole package.
     *************************************************************************/
    public void setAuthority (String authority) {
	this.authority = authority;
    }

    /**************************************************************************
     * Get back the authority serving this package.
     *************************************************************************/
    public String getAuthority() {
	return authority;
    }

    /**************************************************************************
     * Fill in any string representing <em>service
     * notes</em>. According the BioMoby API, the service notes are
     * meant to contain human-readable free text describing further a
     * service returning this package. <p>
     *
     * @see #setServiceNotesXML for adding CDATA section
     *
     * @see #addException for adding service exceptions
     * @param notes human-readable description
     *************************************************************************/
    public void setServiceNotes (String notes) {
	serviceNotes = notes;
	isNotesCDATA = false;
    }

    /**************************************************************************
     * Fill in any string representing <em>service notes</em>. The
     * string will be added as a CDATA (so it can contain any
     * XML). <p>
     *
     * @see #setServiceNotes for adding string that will be properly escaped
     *
     * @see #addException for adding service exceptions
     * @param notes human-readable description
     *************************************************************************/
    public void setServiceNotesXML (String notes) {
	serviceNotes = notes;
	isNotesCDATA = true;
    }

    /**************************************************************************
     * Get back service notes.
     *************************************************************************/
    public String getServiceNotes() {
	return serviceNotes;
    }


    /**************************************************************************
     * Add a new exception reported by this service execution. If the
     * exception reports only about a particular job it is its
     * responsibility to have ID of this job filled in. <p>
     *
     * @param error to be added
     *************************************************************************/
    public void addException (ServiceException error) {
	errors.addElement (error);
    }

    /**************************************************************************
     * Add a new exception reported by this service execution. Add to
     * the exception the job ID of the given job. It is a convenient
     * method. <p>
     *
     * @param error to be added
     * @param job whose ID is aded to the 'error'
     *************************************************************************/
    public void addException (ServiceException error, MobyJob job) {
	error.setJobId (job.getId());
	errors.addElement (error);
    }

    /**************************************************************************
     * Set one or more exceptions reported by this service
     * invocation. It first removes all previously set exceptions. <p>
     *
     * @param errors to be assigned
     *************************************************************************/
    public void setExceptions (ServiceException[] errors) {
	this.errors.clear();
	for (int i = 0; i < errors.length; i++)
	    this.errors.addElement (errors[i]);
    }

    /**************************************************************************
     * Return all so far set exceptions.
     *************************************************************************/
    public ServiceException[] getExceptions() {
	ServiceException[] result = new ServiceException [errors.size()];
	errors.copyInto (result);
	return result;
    }

    /**************************************************************************
     * Evaluate current exceptions. <p>
     *
     * This is a utility (convenient) method that extracts all
     * exceptions from this package and formats them into one string,
     * and either returns it, or throws an exception (depending on the
     * policy in 'throwException'). <p>
     *
     * @param log if not null then the formatted exceptions are
     * written into this log (the severity of the exceptions dedicates
     * the log level used)
     *
     * @param throwException specify what to do when an error
     * condition was found: if 'throwException' is true then an
     * exception is thrown, otherwise the formatted exception is
     * returned
     *
     * @return formatted exceptions (of any kind, not only severe
     * errors), or an empty String if there were no exceptions
     *
     * @throws MobyException if there were at least one severe
     * exceptions and if the 'throwException' policy is true
     *************************************************************************/
    public String evaluateExceptions (org.apache.commons.logging.Log log,
				      boolean throwException)
	throws MobyException {

	boolean errorFound = false;
	StringBuffer buf = new StringBuffer();
	ServiceException[] exs = getExceptions();
	for (int i = 0; i < exs.length; i++) {
	    String msg = exs[i].toString();
	    int severity = exs[i].getSeverity();
	    switch (severity) {
	    case ServiceException.ERROR:
		if (log != null) log.error (msg);
		buf.append (msg);
		buf.append ("\n");
		errorFound = true;
		break;
		
	    case ServiceException.WARNING:
		    if (log != null) log.warn (msg);
		    buf.append (msg);
		    buf.append ("\n");
		    break;
		    
	    case ServiceException.INFO:
		if (log != null) log.info (msg);
		buf.append (msg);
		buf.append ("\n");
		break;
	    }
	}
	if (errorFound && throwException) {
	    throw new MobyException (buf.toString());
	} else {
	    return buf.toString();
	}
    }
    
    /**************************************************************************
     * Return true if this package contains at least one exception of
     * the severity {@link ServiceException#ERROR}). Otherwise return false.
     *************************************************************************/
    public boolean hasAnError() {

	ServiceException[] exs = getExceptions();
	for (int i = 0; i < exs.length; i++) {
	    if (exs[i].getSeverity() == ServiceException.ERROR)
		return true;
	}
	return false;
    }

}
