// ServiceException.java
//
//    Created: December 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.jdom.Element;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

/**
 * A container for an exception raised by a service provider when
 * something wrong has to be reported back to a client. These
 * exceptions are carried in "service notes" in {@link
 * org.biomoby.shared.parser.MobyPackage}. <p>
 *
 * It also contains constants for known error codes and for exception
 * severity levels. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: ServiceException.java,v 1.14 2009/06/09 18:55:35 gordonp Exp $
 */

public class ServiceException
    extends MobyException {

    /** A severity code that corresponds to a fatal error. */
    public static final int ERROR = 1;

    /** A severity code that corresponds to an informative diagnostic
     * message. */
    public static final int WARNING = 2;

    /** A severity code that corresponds to a message not related to
     * any error. */
    public static final int INFO = 3;

    /** Error code: No error. Used together with severity code {@link #INFO} -
     * indicating that actually no error occured and the service was
     * executed normally. */
    public static final int OK = 700;

    /** Error code: Setting input data under a non-existing name, or
     * asking for a result using an unknown name. */
    public static final int UNKNOWN_NAME = 200;

    /** Error code: Input data are invalid; they do not match with
     * their definitions, or with their dependency conditions. */
    public static final int INPUTS_INVALID = 201;

    /** Error code: Used when a client tries to send input data to a
     * job created in a previous call but the server does not any more
     * accept input data. */
    public static final int INPUT_NOT_ACCEPTED = 202;

    /** Error code: Service requires a parameter but none was
     * given. */
    public static final int INPUT_REQUIRED_PARAMETER = 221;

    /** Error code: Given parameter is incorrect. */
    public static final int INPUT_INCORRECT_PARAMETER = 222;

    /** Error code: Given input of type Simple is incorrect. */
    public static final int INPUT_INCORRECT_SIMPLE = 223;

    /** Error code: Service requires two or more data inputs. */
    public static final int INPUT_REQUIRED_PARAMETERS = 224;

    /** Error code: Given input of type Collection is incorrect. */
    public static final int INPUT_INCORRECT_COLLECTION = 225;

    /** Error code: Given an empty input data. */
    public static final int INPUT_EMPTY_OBJECT = 226;

    /** Error code: Incorrect Namespace in the input object. */
    public static final int INPUT_INCORRECT_NAMESPACE = 227;

    /** Error code: The same job (analysis) has already been executed,
     * or the data that had been set previously do not exist or are
     * not accessible anymore. */
    public static final int NOT_RUNNABLE = 300;

    /** Error code: A job (analysis) has not yet been started. Note
     * that this exception is not raised when the job has been already
     * finished. */
    public static final int NOT_RUNNING = 301;

    /** Error code: For some reasons, a job (analysis) is not
     * interruptible, but an attempt to do so was done. */
    public static final int NOT_TERMINATED = 302;

    /** Error code: There are no metadata available for the executed
     * service/analysis. */
    public static final int NO_METADATA_AVAILABLE = 400;

    /** Error code: Used when a service does not agree on using any of
     * the proposed notification protocols. */
    public static final int PROTOCOLS_UNACCEPTED = 500;

    /** Error code: A placeholder for all other errors not defined
     * explicitly in the Biomoby API. */
    public static final int INTERNAL_PROCESSING_ERROR = 600;

    /** Error code: A generic network failure. */
    public static final int COMMUNICATION_FAILURE = 601;

    /** Error code: Used when a service call expects to find an
     * existing state but failed. */
    public static final int UNKNOWN_STATE = 602;

    /** Error code: A requested method is not implemented. */
    public static final int NOT_IMPLEMENTED = 603;


    protected String refQueryID;
    protected String refElement;
    protected int severity = INFO;
    protected int code = OK;
    protected String description;

    static HashMap<Integer,String> severityNames = new HashMap<Integer,String>();
    static {
	severityNames.put (new Integer (ERROR),   "error");
	severityNames.put (new Integer (WARNING), "warning");
	severityNames.put (new Integer (INFO),    "information");
    }

    static HashMap<Integer,String> codeNames = new HashMap<Integer,String>();
    static {
	codeNames.put (new Integer (OK), "OK");
	codeNames.put (new Integer (UNKNOWN_NAME), "UNKNOWN_NAME");
	codeNames.put (new Integer (INPUTS_INVALID), "INPUTS_INVALID");
	codeNames.put (new Integer (INPUT_NOT_ACCEPTED), "INPUT_NOT_ACCEPTED");
	codeNames.put (new Integer (INPUT_REQUIRED_PARAMETER), "INPUT_REQUIRED_PARAMETER");
	codeNames.put (new Integer (INPUT_INCORRECT_PARAMETER), "INPUT_INCORRECT_PARAMETER");
	codeNames.put (new Integer (INPUT_INCORRECT_SIMPLE), "INPUT_INCORRECT_SIMPLE");
	codeNames.put (new Integer (INPUT_REQUIRED_PARAMETERS), "INPUT_REQUIRED_PARAMETERS");
	codeNames.put (new Integer (INPUT_INCORRECT_COLLECTION), "INPUT_INCORRECT_COLLECTION");
	codeNames.put (new Integer (INPUT_EMPTY_OBJECT), "INPUT_EMPTY_OBJECT");
	codeNames.put (new Integer (INPUT_INCORRECT_NAMESPACE), "INPUT_INCORRECT_NAMESPACE");
	codeNames.put (new Integer (NOT_RUNNABLE), "NOT_RUNNABLE");
	codeNames.put (new Integer (NOT_RUNNING), "NOT_RUNNING");
	codeNames.put (new Integer (NOT_TERMINATED), "NOT_TERMINATED");
	codeNames.put (new Integer (NO_METADATA_AVAILABLE), "NO_METADATA_AVAILABLE");
	codeNames.put (new Integer (PROTOCOLS_UNACCEPTED), "PROTOCOLS_UNACCEPTED");
	codeNames.put (new Integer (INTERNAL_PROCESSING_ERROR), "INTERNAL_PROCESSING_ERROR");
	codeNames.put (new Integer (COMMUNICATION_FAILURE), "COMMUNICATION_FAILURE");
	codeNames.put (new Integer (UNKNOWN_STATE), "UNKNOWN_STATE");
	codeNames.put (new Integer (NOT_IMPLEMENTED), "NOT_IMPLEMENTED");
    }

    /**************************************************************************
     * An empty constructor. Sets severity code to {@link #INFO} and
     * error code to {@link #OK}.
     *************************************************************************/
    public ServiceException() {
    }

    /**************************************************************************
     * A usual constructor setting severity, code and message about an
     * exception. It does not specify which job or data it reports about.
     *************************************************************************/
    public ServiceException (int severity, int code, String message) {
	setSeverity (severity);
	setErrorCode (code);
	setMessage (message);
    }

    /**************************************************************************
     * A richer constructor setting more details about an exception.
     *************************************************************************/
    public ServiceException (int severity, int code,
			     String jobId, String dataName) {
	setSeverity (severity);
	setErrorCode (code);
	setJobId (jobId);
	setDataName (dataName);
     }
    
    /**************************************************************************
     * A full constructor setting more details about an exception.
     *************************************************************************/
    public ServiceException (int severity, int code,
			     String jobId, String dataName,
			     String msg) {
	setSeverity (severity);
	setErrorCode (code);
	setJobId (jobId);
	setDataName (dataName);
	setMessage (msg);
     }
    
    /**************************************************************************
     * Create an instance of ServiceException that represents an error. <p>
     *
     * @param code error code
     * @param msg error message
     *************************************************************************/
    public static ServiceException error (int code, String msg) {
	ServiceException ex = new ServiceException();
	ex.setSeverity (ERROR);
	ex.setErrorCode (code);
	ex.setMessage (msg);
	return ex;
    }

    /**************************************************************************
     * Create an instance of ServiceException that represents an error. <p>
     *
     * @param msg error message
     *************************************************************************/
    public static ServiceException error (String msg) {
	ServiceException ex = new ServiceException();
	ex.setSeverity (ERROR);
	ex.setMessage (msg);
	ex.setErrorCode (INTERNAL_PROCESSING_ERROR);
	return ex;
    }

    /**************************************************************************
     * Create an instance of ServiceException that represents a warning. <p>
     *
     * @param msg warning message
     *************************************************************************/
    public static ServiceException warning (String msg) {
	ServiceException ex = new ServiceException();
	ex.setSeverity (WARNING);
	ex.setMessage (msg);
	return ex;
    }

    /**************************************************************************
     * Create an instance of ServiceException that represents an info. <p>
     *
     * @param msg info message
     *************************************************************************/
    public static ServiceException info (String msg) {
	ServiceException ex = new ServiceException();
	ex.setSeverity (INFO);
	ex.setMessage (msg);
	return ex;
    }

    /**************************************************************************
     * Return an identifier of an offending job.
     *************************************************************************/
    public String getJobId() { return refQueryID; }
    
    /**************************************************************************
     * Set an identifier of an offending job.
     *************************************************************************/
    public void setJobId (String id) { refQueryID = id; }
    
    /**************************************************************************
     * Set an identifier of an offending job. <p>
     *
     * @param job a job whose Id is used
     *************************************************************************/
    public void setJobId (MobyJob job) { refQueryID = job.getId(); }
    
    
    /**************************************************************************
     * Return an article (data) name of an offending data input.
     *************************************************************************/
    public String getDataName() { return refElement; }
    
    /**************************************************************************
     * Set an article name of an offending data input.
     *************************************************************************/
    public void setDataName (String name) { refElement = name; }


    /**************************************************************************
     * Return an error message associated with this exception. <p>
     *
     * @return whatever was previously set by {@link #setMessage},
     * or a default description (if nothing was set and the error code
     * is known to this class), or null.
     *************************************************************************/
    public String getMessage() {
	if (description != null)
	    return description;
	return (String)codeNames.get (new Integer (code));
    }
    
    /**************************************************************************
     * Assign an error message to this exception.
     *************************************************************************/
    public void setMessage (String msg) { description = msg; }


    /**************************************************************************
     * Return the current severity level. <p>
     *
     * @see #ERROR
     * @see #WARNING
     * @see #INFO
     *************************************************************************/
    public int getSeverity() { return severity; }

    /**************************************************************************
     * Return the current severity level in a stringified form. <p>
     *************************************************************************/
    public String getSeverityAsString() {
	return (String)severityNames.get (new Integer (severity));
    }

    /**************************************************************************
     * Set severity level.
     *************************************************************************/
    public void setSeverity (int severity) {
	if (severityNames.containsKey (new Integer (severity)))
	    this.severity = severity;
	else
	    this.severity = OK;
    }

    /**************************************************************************
     * Set severity level.
     *************************************************************************/
    public void setSeverity (String severityName) {
	for (Iterator it = severityNames.entrySet().iterator(); it.hasNext(); ) {
	    Map.Entry entry = (Map.Entry)it.next();
	    String name = (String)entry.getValue();
	    if (name.equals (severityName)) {
		severity = ((Integer)entry.getKey()).intValue();
		return;
	    }
	}
	severity = OK;
    }


    /**************************************************************************
     * Return an error code associated with this exception. <p>
     *************************************************************************/
    public int getErrorCode() { return code; }

    /**************************************************************************
     * Return a stringified form of the error code associated with
     * this exception. <p>
     *
     * @return a stringified error code or an empty string (if the
     * code is service-specific)
     *************************************************************************/
    public String getErrorCodeAsString() {
	String str = (String)codeNames.get (new Integer (code));
	return (str == null ? "" : str);
    }

    /**************************************************************************
     * Assign an error code to this exception.
     *************************************************************************/
    public void setErrorCode (int code) { this.code = code; }


    /**************************************************************************
     *
     *************************************************************************/
    public String toString() {
	StringBuffer buf = new StringBuffer();
	buf.append ("Exception: ");
	buf.append (getSeverityAsString());
	buf.append (", ");
	String str = getErrorCodeAsString();
	buf.append ("".equals (str) ? "" + code : str);
	if (refQueryID != null) {
	    buf.append (", in job: ");
	    buf.append (refQueryID);
	}
	if (refElement != null) {
	    buf.append (", concerning: ");
	    buf.append (refElement);
	}
	String desc = getMessage();
	if (desc != null) {
	    buf.append ("\n\t");
	    buf.append (desc);
	}
	return new String (buf);
    }

    /**************************************************************************
     * Return the same contents as {@link #toString} method but
     * indented by level expressed in the parameter 'indent'. <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);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public Element toXML() {
	Element elem = MobyPackage.getXMLElement (MobyTags.MOBYEXCEPTION);
	MobyPackage.setXMLAttribute (elem, MobyTags.SEVERITY, getSeverityAsString());
	if (refQueryID != null)
	    MobyPackage.setXMLAttribute (elem, MobyTags.REFQUERYID, refQueryID);
	if (refElement != null)
	    MobyPackage.setXMLAttribute (elem, MobyTags.REFELEMENT, refElement);
	Element elemCode = MobyPackage.getXMLElement (MobyTags.EXCEPTIONCODE);
	elemCode.setText (""+code);
	elem.addContent (elemCode);
	String desc = getMessage();
	if (desc != null) {
	    Element elemDesc = MobyPackage.getXMLElement (MobyTags.EXCEPTIONMESSAGE);
	    elemDesc.setText (desc);
	    elem.addContent (elemDesc);
	}
	return elem;
    }

    /**************************************************************************
     * Extract all exceptions from a <em>serviceNotes</em> XML
     * element. This is a convenient method that can be used when
     * dealing with an XML response from a service without parsing the
     * whole response to a {@link MobyPackage}. <p>
     *
     * @param serviceNotes a piece of XML
     * <tt>&lt;serviceNotes&gt;...&lt;/serviceNotes&gt;</tt>
     *
     * @return an array, potentially an empty array, of all exceptions
     * extracted from the 'serviceNotes'
     *************************************************************************/
    @SuppressWarnings("unchecked")
    public static ServiceException[] extractExceptions (Element serviceNotes) {
	if (serviceNotes == null)
	    return new ServiceException[] {};

	Vector<ServiceException> v = new Vector<ServiceException>();
	for (Iterator<Element> it =
		 serviceNotes.getChildren (MobyTags.MOBYEXCEPTION).iterator();
	     it.hasNext(); ) {
	    ServiceException ex = extractException (it.next());
	    if (ex != null)
		v.addElement (ex);
	}
	for (Iterator<Element> it =
		 serviceNotes.getChildren (MobyTags.MOBYEXCEPTION, JDOMUtils.MOBY_NS).iterator();
	     it.hasNext(); ) {
	    ServiceException ex = extractException (it.next());
	    if (ex != null)
		v.addElement (ex);
	}
	ServiceException[] result = new ServiceException [v.size()];
	v.copyInto (result);
	return result;
    }

    /**************************************************************************
     * Extract one exception from an XML element 'mobyException'.
     *************************************************************************/
    protected static ServiceException extractException (Element elem) {
	ServiceException ex = new ServiceException();
	String severity = elem.getAttributeValue (MobyTags.SEVERITY);
	if (severity == null)
	    severity = elem.getAttributeValue (MobyTags.SEVERITY, JDOMUtils.MOBY_NS);
	ex.setSeverity (severity);
	String codeStr = JDOMUtils.getChildText (elem, MobyTags.EXCEPTIONCODE);
	try {
	    ex.setErrorCode (new Integer (codeStr).intValue());
	} catch (Exception e) {
	}
	ex.setMessage (JDOMUtils.getChildText (elem, MobyTags.EXCEPTIONMESSAGE));
	return ex;
    }

     public String toXMLString(){
 	StringBuffer xml = new StringBuffer("          <moby:" + MobyTags.MOBYEXCEPTION + " " +
 					    MobyTags.SEVERITY + "=\"" + severity + "\" ");
 	if(refQueryID != null){
 	    xml.append(MobyTags.REFQUERYID + "=\"" + refQueryID + "\" ");
 	}
 	if(refElement != null){
 	    xml.append(MobyTags.REFELEMENT + "=\"" + refElement + "\" ");
 	}
 	xml.append(">\n");

 	xml.append("<moby:"+ MobyTags.EXCEPTIONCODE + ">"+code+"</moby:"+MobyTags.EXCEPTIONCODE+">");

 	if(getMessage() != null){
 	    xml.append("            <moby:"+MobyTags.EXCEPTIONMESSAGE+">"+getMessage().replaceAll("<","&lt;")+"</moby:"+MobyTags.EXCEPTIONMESSAGE+">");
 	}

 	xml.append("          </moby:" + MobyTags.MOBYEXCEPTION + ">");

 	return xml.toString();
     }

}
