package org.biomoby.client;

import java.io.*;
import java.net.Authenticator; // for HttpURLConnection username/password
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication; // for HttpURLConnection username/password
import java.net.URL;
import java.util.*;
import java.util.logging.*;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.rpc.Service;
import javax.xml.transform.TransformerException;

import org.apache.axis.client.Call;
import org.apache.axis.message.MessageElement;
import org.apache.xml.utils.PrefixResolver;
import org.apache.xml.utils.PrefixResolverDefault;

// import org.apache.xpath.XPath;
// import org.apache.xpath.XPathContext;
// import org.apache.xpath.objects.XNodeSet;
// import org.apache.xpath.objects.XObject;
import javax.xml.xpath.*;

import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;  // defined the Moby XML element names
import org.biomoby.w3c.addressing.EndpointReference;
import org.omg.lsae.notifications.AnalysisEvent;

import org.w3c.dom.*;
import ca.ucalgary.services.util.IOUtils;

/**
 * This class handles the WSDL transaction to request a response
 * from a remote SOAP Web service that handles the 
 * <a href="http://www.biomoby.org">MOBY</a> format.  It depends on 
 * having already retrieved the definition of the Web service via 
 * the MOBY central registry using the 
 * <a href="http://www.biomoby.org/moby-live/Java/docs/index.html">jMOBY</a> API,
 * and for now it uses the 
 * <a href="http://ws.apache.org/axis/index.html">Apache
 * Axis</a> Web services framework, as well as Apache Xalan.  There are code comments for the
 * few lines that rely on Axis classes rather than the JAX-RPC interfaces.
 *
 * @author Paul Gordon gordonp@ucalgary.ca
 */
public class MobyRequest{

    protected MobyService mobyService = null;
    protected MobyContentInstance inputData = null;
    protected MobyContentInstance outputData = null;
    protected Central mobyCentral = null;
    protected PrefixResolver mobyPrefixResolver = null;
    
    protected Hashtable wsdlCache = null;
    protected String lastWsdlCacheKey = null;
    protected DocumentBuilder docBuilder = null;
    protected Service service = null;
    
    protected Class stringType;
    protected static boolean debug = false;
    protected PrintStream debugPS = System.err;
    protected String responseString = null;

    private XPathExpression stringEncodedXPath;
    private XPathExpression base64EncodedXPath;
    private String user;
    private String password;

    private int autoID = 0;

    // Used as invocation callback if MobyRequest is acting as a server,
    // or is executing services as a client asynchronously
    private Vector<MobyRequestEventHandler> eventHandlers; 

    private static Logger logger = Logger.getLogger(MobyRequest.class.getName());

    /**
     * Default constructor.  You should have a Central instance around since you're going to 
     * be retrieving MobyServices to pass into here. Lets reuse it.
     *
     * @param central An instance of a Moby central object so we can make requests about object types, etc.
     * @throws ParserConfigurationException if JAXP doesn't have any valid DOM-building XML parsers set up for use
     */
    public MobyRequest(Central central) throws ParserConfigurationException{
	mobyCentral = central;
	wsdlCache = new Hashtable();

	eventHandlers = new Vector<MobyRequestEventHandler>();

	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	dbf.setNamespaceAware(true);	
	docBuilder = dbf.newDocumentBuilder();

	try{
	    stringType = Class.forName("java.lang.String");
	}catch(ClassNotFoundException classe){
	    debugPS.println("WARNING: Something is very wrong, could not find Class definition of String: " + classe);
	}

	// Now compile the XPath statements that will be used fetch data from the server response
	try{
	    XPath xPath = (new org.apache.xpath.jaxp.XPathFactoryImpl()).newXPath();
	    xPath.setNamespaceContext(new NamespaceContextImpl());
	    base64EncodedXPath = xPath.compile("//*[starts-with(substring-after(@"+
					       MobyPrefixResolver.XSI1999_PREFIX+
					       ":type, ':'), \"base64\") or starts-with(substring-after(@"+
					       MobyPrefixResolver.XSI2001_PREFIX+
					       ":type, ':'), \"base64\")]");
	    stringEncodedXPath = xPath.compile("//*[substring-after(@"+
					       MobyPrefixResolver.XSI1999_PREFIX+
					       ":type, ':')=\"string\" or substring-after(@"+
					       MobyPrefixResolver.XSI2001_PREFIX+
					       ":type, ':')=\"string\"] | //"+
					       MobyPrefixResolver.SOAP_ENC_PREFIX+":string");
	}
	catch(XPathExpressionException xpee){
	    debugPS.println("Syntax error encountered while compiling XPath " +
			    "statements for internal use (code bug?): " + xpee);
	}
	setDebugMode(Boolean.getBoolean("moby.debug"));
    }

    public void setAuthentication(String user, String password){
        this.user = user;
        this.password = password;
    }
    
    /**
     * @param mode if true, debugging information is printed to the stream returned by getDebugOutputStream
     */
    public void setDebugMode(boolean mode){
	debug = mode;
    }

    /**
     * Standard error is used unless this method is called.
     *
     * @param ps the OutputStream to which debugging information is sent.
     * @throws IllegalArgumentException if the stream is null
     */
    public void setDebugPrintStream(PrintStream ps) throws IllegalArgumentException{
	if(ps == null){
	    throw new IllegalArgumentException("The OutputStream specified to MobyRequest was null");
	}
	debugPS = ps;
    }

    /**
     * @return the instance of the class implementing Central that we are using
     */
    public Central getCentralImpl(){
	return mobyCentral;
    }

    /**
     * @param mobyservice the MobyService that should be executed when invokeService is called
     */
    public void setService(MobyService mobyservice){
	if(mobyservice == null){
	    mobyService = null;
	}
	else if(mobyService == null || !mobyservice.equals(mobyService)){
	    mobyService = mobyservice;
	}
    }

    /**
     * @return the MobyService that will be executed when invokeService is called
     */
    public MobyService getService(){
	return mobyService;
    }

    /**
     * @return the Raw MOBY XML response as a string
     */
    public String getResponseXML(){
	return responseString;
    }

    /**
     * Sets the input data for the MOBY service request. The length of the input array, and the
     * number of input parameters required by the service must be equal when invokeService() is called.
     * This method strictly enforces that the input be of the appropriate type for the service.
     *
     * Note that there is no requirement to use MobyDataInstance.setXmlMode() before passing in
     * data, this class will temporarily set the XML mode of the data when it is required.
     *
     * @throws IllegalArgumentException if the input does not fit the criteria of the service (e.g. wrong data type)
     */
    public void setInput(MobyContentInstance data) throws MobyException{
	inputData = data;
    }

    /**
     * Takes the data in the array, with their current articleNames, as input for the service 
     */
    public void setInput(MobyDataInstance[] data) throws MobyException{
	MobyDataJob job = new MobyDataJob();
	for(MobyDataInstance param: data){
	    job.put(param.getName(), param);
	}
	inputData = new MobyContentInstance();
	inputData.put(job);
    }

    /**
     * Convenience method to run services that take one argument.  If the service
     * requires the input to have a name, it will be automatically assigned.
     */
    public void setInput(MobyDataInstance datum) throws MobyException{
	setInput(datum, "");
    }

    /**
     * Convenience method to run services that take one named argument.
     */
    public void setInput(MobyDataInstance datum, String paramName) throws MobyException{
        inputData = new MobyContentInstance(datum, paramName);
    }

    /**
     * @return the MobyService that will be executed when invokeService is called
     */
    public MobyContentInstance getInput(){
	return inputData;
    }
    
    /**
     *  Same functionality as setSecondaryInput(MobyDataSecondaryInstance[])
     */
    public void setSecondaryInput(Collection<MobyDataSecondaryInstance> secondaryData) throws MobyException{
	setSecondaryInput(secondaryData.toArray(new MobyDataSecondaryInstance[secondaryData.size()]));
    }

    /**
     * This method will assign the provided secondary parameters to all primary input data currently
     * in this object.  This is convenient if you are running 100 seqs through BLAST and only want to set
     * the parameters once.  If you instead want to set secondary input differently for all primary inputs, you'll
     * need to create a custom MobyContentInstance as input to setInput().
     *
     * @throws MobyException if a parameter name is blank, or overrides a primary parameter
     */
    public void setSecondaryInput(MobyDataSecondaryInstance[] secondaryData) throws MobyException{

	Iterator queryNames = inputData.keySet().iterator();
	// For each query
	while(queryNames.hasNext()){
	    MobyDataJob queryParams = inputData.get(queryNames.next());
	    // Set all the secondary params (overwrites any old ones)
	    for(int i = 0; i < secondaryData.length; i++){
		String secName = secondaryData[i].getName();
		if(secName == null || secName.length() == 0){
		    throw new MobyException("A secondary parameter cannot have a blank name (array index " + i + ")");
		}
		if(queryParams.containsKey(secName) && queryParams.get(secName) instanceof MobyPrimaryData){
		    throw new MobyException("A secondary parameter cannot override an existing primary parameter " +
					    "with the same name (" + secName + ")");
		}
		queryParams.put(secName, secondaryData[i]);
	    }
	}
    }

    /**
     * @return a vector of MobyDataInstance[], each element of the vector is the collection of response objects for the correspondingly indexed input request.
     *
     * @throws MobyException if you try to get the results before calling InvokeService
     */
    public MobyContentInstance getOutput() throws MobyException{
	if(outputData == null){
	    throw new MobyException("Trying to access MOBY service results " +
				    "before the service is invoked");
	}
	else{
	    return outputData;
	}
    }

    /**
     * The main method of the class.  If all of the MOBY input objects 
     * are properly defined according to the Web service definition,
     * a SOAP request will be sent to the remote server, and the method 
     * will return one or more MOBY objects (synchronous).  
     * Call this method after calling setService, and setInput.  If you do not call
     * setSecondaryInput, the default secondary parameter values will be used.
     *
     * @return the results of the remote Web service in response to the give input
     *
     * @throws MobyException i.e. there was something wrong with the input, output or remote service's logic
     * @throws SOAPException i.e. there was a problem with the underlying transaction/transport layer
     */
    public MobyContentInstance invokeService() throws Exception, MobyException, SOAPException, NoSuccessException{
	return mobyService.isAsynchronous() ? invokeService(inputData, new StringBuffer()) : invokeService(inputData, (StringBuffer) null);
    }

    // Used internally for asynchronous thread calls that all need the XML data
    // and can't rely on the answer from thread-insensitive getResponseXML()
    private MobyContentInstance invokeService(MobyContentInstance inData, StringBuffer contentsXML) 
	throws Exception, MobyException, SOAPException, NoSuccessException{
	return invokeService(inData, contentsXML, null, 0);
    }

    private MobyContentInstance invokeService(MobyContentInstance inData, StringBuffer contentsXML, MobyRequestEventHandler handler, int requestId) 
	throws Exception, MobyException, SOAPException, NoSuccessException{

	if(mobyService == null){
	    throw new MobyException("Tried to invoke null service from MobyRequest (call setService first)");
	}

	Element mobyDOM = null;
	if(mobyService.isAsynchronous()){
	    // Async is "simpler", because it had to merge DOMs together into a single MobyContentInstance anyway
	    MobyContentInstance mci = performAsyncSOAPRequest(mobyService, inData, handler, requestId);
	    StringWriter writer = new StringWriter();
	    MobyDataUtils.toXMLDocument(writer, mci);
	    contentsXML.append(writer.toString());
	    return mci;
	}
	else{
	    String mobyXML = convertMOBYDataToMOBYRequest(inData);
//PG 	    Call call = getServiceFromWSDL();
//PG 	    if(user != null && password != null) {
//PG 	        call.setProperty( Call.USERNAME_PROPERTY, user );
//PG 	        call.setProperty( Call.PASSWORD_PROPERTY, password );
//PG 	    }
//PG	    mobyDOM = performSOAPRequest(call, mobyXML, contentsXML);
	    mobyDOM = performSOAPRequest(new URL(mobyService.getURL()), mobyService.getName(), mobyXML, contentsXML);
	    // The following parses the DOM and extracts all the appropriate jMOBY objects to represent the XML in Java
	    return MobyDataUtils.fromXMLDocument(mobyDOM, mobyService.getServiceType().getRegistry());  
	}
    }

    protected MobyContentInstance performAsyncSOAPRequest(MobyService mservice, MobyContentInstance inData, 
							  MobyRequestEventHandler handler, int requestId) 
	throws Exception{
	String mobyXML = convertMOBYDataToMOBYRequest(inData);  
	EndpointReference epr = AsyncClient.sendRequest(mservice, mobyXML);

	// Essentially cloning, so removing ids doesn't change the 
	// MobyContentInstance "data" (which we will use again later on)
	MobyContentInstance finalContents = new MobyContentInstance();
	Set<String> queryIDs = new HashSet<String>(inData.keySet());
	try {
	    // Should add some timeout here...
	    while(!queryIDs.isEmpty()){
		// todo: make this setable
		Thread.sleep(5000);

		AnalysisEvent[] events = 
		    AsyncClient.poll(epr, queryIDs);

		Vector<String> newDataAvailable = new Vector<String>();
		for(AnalysisEvent event: events){
		    if(event != null && event.isCompleted()){
			queryIDs.remove(event.getQueryId());
			newDataAvailable.add(event.getQueryId());
		    }
		}

		if(newDataAvailable.size() > 0){
		    // Parse and merge the new data into the existing contents
		    InputStream resultStream = AsyncClient.getResultStream(epr, newDataAvailable);
		    Element mobyDOM = asyncSoapTextToMobyDOM(resultStream);
		    MobyContentInstance newResults = MobyDataUtils.fromXMLDocument(mobyDOM, mservice.getServiceType().getRegistry());
		    // The merge
		    for(String jobid: newResults.keySet()){
			finalContents.put(jobid, newResults.get(jobid));
		    }
		    
		    // Inform the handler that some data has been added to the response (for incremental display?)
		    if(handler != null){
			MobyRequestEvent mre = new MobyRequestEvent(finalContents, this, mservice, inData, null, requestId);
			StringWriter xmlWriter = new StringWriter();
			MobyDataUtils.toXMLDocument(xmlWriter, finalContents);
			
			mre.setContentsXML(xmlWriter.toString());
			if(!queryIDs.isEmpty()){
			    // Send an update event only if we aren't finished yet.
			    // If we are finished, the client is going to get this event as the 
			    // invocation thread finishes up (no need to double up).
			    handler.processEvent(mre);
			}
		    }
		}
	    }
	} catch (Exception e) {
	    e.printStackTrace();
	    AsyncClient.destroy(epr);
	    throw new Exception("Exception occured while polling the service invocation: " + e);
	}

	return finalContents;
    }

    private Element asyncSoapTextToMobyDOM(InputStream inStream) throws Exception{
	Element soapDOM = null;
	synchronized(docBuilder){
	    soapDOM = docBuilder.parse(inStream).getDocumentElement();
	}
	final boolean IS_ASYNC_SERVICE_CALL = true;
	return decodeSOAPMessage(soapDOM,  null,  null, IS_ASYNC_SERVICE_CALL);
    }

    /**
     * Asynchronous call to invokeService.  A callback to the passed-in handler will be made when 
     * the response is ready, or there is an exception.
     *
     * @return the id that the callback event will return from getID(), allowing a client to distinguish between multiple concurrent invocation callbacks
     */
    public synchronized int invokeService(MobyRequestEventHandler handler){
	final int id = autoID++;

	try{
	    Thread t = new InvocationThread(this, inputData, handler, id);  // see internal class definition below
	    t.start();
	} catch(final Exception e){
	    // Launching callback in new Thread avoids possible deadlocks caused by handler 
	    // calling invokeService() again as a retry 
	    final MobyRequestEventHandler h = handler;
	    final MobyContentInstance i = inputData;
	    final MobyService s = getService();
	    final MobyRequest t = this;
	    (new Thread(){public void run(){h.processEvent(new MobyRequestEvent(i, t, s, null, e, id));}}).start();
	}

	return id;
    }

    // This is the class that asynchronously calls the service and does a callback to 
    // the handler specified in the invocation.
    class InvocationThread extends Thread {
	MobyContentInstance data;
	ByteArrayOutputStream dataXML; 
	MobyService mservice;
	MobyRequest mobyRequest; 
	MobyRequestEventHandler handler;
	int requestId;

	InvocationThread(MobyRequest mr, MobyContentInstance inData, MobyRequestEventHandler h, int id) throws Exception{
	    data = inData;
	    mobyRequest = mr;
	    mservice = mobyRequest.getService();
	    handler = h;
	    requestId = id;

	    dataXML = new ByteArrayOutputStream();
	    MobyDataUtils.toXMLDocument(dataXML, data);

	    // Name the thread after the service being run, mostly for ease of debugging
	    setName(mservice.getName()+requestId);
	}

	public void run() {

	    MobyRequestEvent requestEvent = new MobyRequestEvent(data, mobyRequest, mservice, data, null, requestId);
	    // Tell the handler we're starting the request, with the given data
	    handler.start(requestEvent);

	    MobyRequestEvent responseEvent = null;
	    MobyContentInstance content = null;
	    StringBuffer contentsXML = new StringBuffer();  //to be filled in by the RPC call below
	    try{
		content = mobyRequest.invokeService(data, contentsXML, handler, requestId); //RPC call...
	    }
	    catch(Exception e){
		responseEvent = new MobyRequestEvent(content, mobyRequest, mservice, data, e, requestId);
	    }
	    catch(Error err){
		responseEvent = new MobyRequestEvent(content, mobyRequest, mservice, data, err, requestId);
	    }

	    if(responseEvent == null){
		responseEvent = new MobyRequestEvent(content, mobyRequest, mservice, data, null, requestId);
	    }
	    // We've got the raw XML laying around, so why not provide it unmolested to the callback?
	    responseEvent.setContentsXML(contentsXML.toString());
	    handler.processEvent(responseEvent);
	    handler.stop(mobyRequest, requestId);
	}
    }

    public void addEventHandler(MobyRequestEventHandler h){
	eventHandlers.add(h);
    }

    public void removeEventHandler(MobyRequestEventHandler h){
	eventHandlers.remove(h);
    }

    public void sendResponse(MobyRequestEvent mre){
	// Not yet implemented, need to conform to some web.xml specification here...
    }

    /**
     * Creates the SOAP Call that will be invoked later.  This should be based on the WSDL document
     * and parameter information from the MobyService, but these are currently not up to snuff.
     */
    protected Call setCallFromWSDL(String wsdl) throws MobyException, SOAPException{
	if(service == null){
	    service = new org.apache.axis.client.Service();  // AXIS SPECIFIC This acts as a factory for Calls
	}

	Call soapCall;
	try{
	    soapCall = (Call) service.createCall();//create a fresh Call each time
	}catch(javax.xml.rpc.ServiceException se){
	    throw new SOAPException("Could not instatiate call to SOAP Service: " + se);
	}

	// Should initialize endpoint, etc. This call is AXIS SPECIFIC, otherwise you'll 
	// have to do the call's info setting manually.
	//((org.apache.axis.client.Call) soapCall).setSOAPService(soapService); 
	soapCall.removeAllParameters();
	soapCall.setTargetEndpointAddress(mobyService.getURL());
	soapCall.setPortName(new QName("http://biomoby.org/", 
					   mobyService.getName() + "PortType"));
	//soapCall.setOperationName(new QName("http://biomoby.org/", 
	//				    mobyService.getName()));
	soapCall.setSOAPActionURI("http://biomoby.org/#" + mobyService.getName());
	return soapCall;
    }

    /**
     *  Calls the invoke() method of the JAX-RPC Call interface.
     */
//PG    protected Element performSOAPRequest(Call soapCall, String mobyInputXML, StringBuffer contentsXMLOutput) throws SOAPException{
    protected Element performSOAPRequest(URL endpoint, String method, String mobyInputXML, StringBuffer contentsXMLOutput) throws Exception{
	// First, turn the input objects into a MOBY XML request
//PG	String[] mobyXMLInputData = new String[1];

	//Setup
//PG        mobyXMLInputData[0] = mobyInputXML;

	String soapAction = "http://biomoby.org/#" + method;
	if(user != null && password != null) {
	    final String login = user;
	    final String pass = password;
	    Authenticator.setDefault(new Authenticator() {
		    protected PasswordAuthentication getPasswordAuthentication() {
			return new PasswordAuthentication (login, pass.toCharArray());
		    }
		});
	}
	HttpURLConnection conn = (HttpURLConnection) endpoint.openConnection();
	byte[] payload = ("<ns1:"+method+
			  " soapenv:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:ns1=\"http://biomoby.org/\">"+
			  (mobyInputXML == null ? "" : "<ns1:arg0 xsi:type=\"soapenc:string\" xmlns:soapenc=\"http://schemas.xmlsoap.org/soap/encoding/\">"+mobyInputXML.replaceAll("<","&lt;").replaceAll(">","&gt;")+"</ns1:arg0>")+
			  "</ns1:"+method+">").getBytes();
	IOUtils.writeToConnection(conn, payload, soapAction);
	Node resultSource = IOUtils.readFromConnection(conn);

//PG 	if(debug)
//PG 	    debugPS.println("returnType just before invoke call is " + soapCall.getReturnType());
//PG 	Object returnedObject = null;
//PG 	try{
//PG 	    returnedObject = soapCall.invoke(new QName("http://biomoby.org/", 
//PG 					    mobyService.getName()), mobyXMLInputData);
//PG 	}
//PG 	catch(Exception e){
//PG 	    e.printStackTrace();
//PG 	    //System.err.println("Input: "+mobyInputXML);
//PG 	    throw new SOAPException("While invoking SOAP Call: " + e);
//PG 	}

	try{
//PG 	    if(debug){
//PG 		debugPS.println("SOAP Response was:\n");
//PG 		debugPS.println(soapCall.getResponseMessage().getSOAPPart().getEnvelope());
//PG 	    }
//PG	    Element resultDom = ((MessageElement) soapCall.getResponseMessage().getSOAPPart().getEnvelope()).getAsDOM();
	    Element resultDom = resultSource instanceof Element ? (Element) resultSource : resultSource.getOwnerDocument().getDocumentElement();
	    return decodeSOAPMessage(resultDom, contentsXMLOutput, mobyInputXML);
	} catch(Exception e){
	    e.printStackTrace();
	    throw new SOAPException("Could not get SOAP response as DOM Element: "+ e);
	}

    }

    public Element decodeSOAPMessage(Element n, StringBuffer contentsXMLOutput, String inputXML)
	throws SOAPException, MobyException{
	return decodeSOAPMessage(n, contentsXMLOutput, inputXML, false);
    }

    /**
     * Isolates the MOBY Data from the SOAP message returned by the remote service host.
     *
     * @throws SOAPException if the MOBY payload cannot be found in the SOAP message
     * @throws MobyException if the MOBY message is not well-formed XML
     *
     * @return The root element of the MOBY response DOM
     */
    public Element decodeSOAPMessage(Element n, StringBuffer contentsXMLOutput, String inputXML, boolean async) 
	throws SOAPException, MobyException{
	if(n == null){
	    throw new SOAPException("SOAP Message given to decode is null");
	}

	NodeList node_list = null;
	if(async){
	    node_list = n.getElementsByTagNameNS(MobyPrefixResolver.WSRP_NAMESPACE, 
						 AsyncClient.WSRP_MULTI_PROPERTY_TAG_NAME+"Response");
	}
	else{
	    if(n.getLocalName().equals(mobyService.getName()+"Response") ||
	       n.getLocalName().equals(mobyService.getName())){
		node_list = new MobyPrefixResolver.MobyNodeList();
		((MobyPrefixResolver.MobyNodeList) node_list).add(n);
	    }
	    if(node_list == null || node_list.getLength() == 0){
		node_list = n.getElementsByTagNameNS(MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE,
						     mobyService.getName()+"Response");
	    }
	    if(node_list == null || node_list.getLength() == 0){
		node_list = n.getElementsByTagNameNS(MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE,
						     mobyService.getName());
	    }
	    if(node_list == null || node_list.getLength() == 0){
		node_list = n.getElementsByTagName(mobyService.getName()+"Response");
	    }
	    if(node_list == null || node_list.getLength() == 0){
		node_list = n.getElementsByTagName(mobyService.getName());
	    }
	}

	if(node_list == null || node_list.getLength() == 0){
	    throw new SOAPException("Could not find a response element in SOAP payload (service " +
				    mobyService.getName() + ", root element "+n.getNodeName()+")");
	}

	if(node_list.getLength() > 1){
	    throw new SOAPException("Found more than one response element in SOAP payload, " +
				    "unable to resolve ambiguity of the payload (service provider error?)");
	}

	Node[] responseNodes = null;
	if(async){
	    Vector<Node> nodes = new Vector<Node>();
	    NodeList resultNodeList = node_list.item(0).getChildNodes();
	    for(int i = 0; resultNodeList != null && i < resultNodeList.getLength(); i++){
		if(!(resultNodeList.item(i) instanceof Element)){
		    continue;
		}
		Element resultElement = (Element) resultNodeList.item(i);
		if(resultElement.getLocalName().startsWith(AsyncClient.MOBY_RESULT_PROPERTY_PREFIX)){
		    nodes.add(resultElement);
		}
	    }
	    responseNodes = nodes.toArray(new Node[nodes.size()]);
	}
	else{
	    responseNodes = new Node[]{node_list.item(0)};
	}

	Element domRoot = null;  // Where the result will be put

	for(Node responseNode: responseNodes){
	// Find base64 encoded elements in the SOAP message using XPath and 
	// replace them with the real decoded contents
	node_list = null;
	try{
            node_list = runXPath(base64EncodedXPath, responseNode);
        }
	catch(XPathExpressionException xpee){
            throw new SOAPException("Cannot select base64 encoded SOAP nodes due to exception "+
        			    "while executing XPath statement:" +xpee);
	}
	if(debug && node_list != null){
	    debugPS.println("There were " + node_list.getLength() + 
				" base64 encoded elements in the data");
	}

	// Do decoding for each base64 part found
	for(int i = 0; node_list != null && i < node_list.getLength(); i++){
	    org.w3c.dom.Node change = node_list.item(i);
	    /* Make sure the text data is all put into one contiguous piece for decoding*/
	    change.normalize(); 
	    
	    byte[] decodedBytes = org.apache.axis.encoding.Base64.decode(change.getFirstChild().getNodeValue());
	    String newText = new String(decodedBytes);
	    if(debug){
		debugPS.println("New decoded text is" + newText);
	    }
	    
	    // Swap out this node for the decoded data
	    change.getParentNode().replaceChild(n.getOwnerDocument().createTextNode(new String(decodedBytes)), 
						change);
	}

	// Now see if there are any strings that need decoding
	node_list = null;
	try{
            node_list = runXPath(stringEncodedXPath, responseNode);
        }
	catch(XPathExpressionException xpee){
            throw new SOAPException("Cannot select string encoded SOAP nodes due to exception "+
        			    "while executing XPath statement:" +xpee);
	}

	// Do concatenation for each plain string part found
	for(int i = 0; node_list != null && i < node_list.getLength(); i++){
	    org.w3c.dom.Node change = node_list.item(i);
	    /* Make sure the text data is all put into one contiguous piece for decoding*/
	    change.normalize();
	    String plainString = "";
	    int j = 0;
	    for(NodeList children = change.getChildNodes(); 
		children != null && j < children.getLength();
		j++){
		Node child = children.item(j);
		if(child instanceof CDATASection || child instanceof Text){
		    plainString += child.getNodeValue();
		    if(debug){
			debugPS.println("Plain string is now " + plainString);
		    }
		}
	    }

	    // Swap out this node for the decoded data
	    change.getParentNode().replaceChild(n.getOwnerDocument().createCDATASection(plainString), change);
	}
	if(debug && node_list != null){
	    debugPS.println("There were " + node_list.getLength() + 
				" XML Schema string encoded elements in the data");
	}

	// Parse the MOBY XML document payload
	responseNode.normalize();
	NodeList children = responseNode.getChildNodes();
	if(children == null){
	    throw new MobyException("The MOBY payload has no contents at all"); 
	}
	if(children.getLength() != 1){
	    if(debug){
		debugPS.println("Warning: MOBY Payload appears to have more than " +
			       "just text in it, skipping the non-text sections");
	    }
	}

	Element predefinedDOM = null;  // Choice of ripping DOM Element for moby payload out of SOAP DOM
	String localResponseString = "";  // or storing raw XML strings, to be converted to a DOM later
	for(int j = 0; j < children.getLength(); j++){
	    Node child = children.item(j);
	    if(child instanceof CDATASection || child instanceof Text){
		// Unescape XML special characters in the string, so we can later on 
		// parse the payload as regular XML.
		// Ignore whitespace-only node
		if(child.getNodeValue().matches("^\\s+$")){
		    continue;
		}
		if(debug){
		    debugPS.println("Concatenating text in response " + child.getNodeValue()); 
		}
		localResponseString += child.getNodeValue();//.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("(&amp;|&#x46;)", "&");
	    }
	    if(child instanceof Element && child.getLocalName().equals(MobyTags.MOBY)){
		debugPS.println("Warning: The MOBY contents was found as raw XML inside the SOAP response!\n" +
				"This is illegal according to the MOBY-API, please inform the service\n " +
				" provider, as parsing such text may not be supported in the future");
		localResponseString = null;
		// Store the moby payload root element's DOM represntation, so we don't
		// have to serialize it to the localResponseString and then parse it out 
		// again (that would be wasteful).
		predefinedDOM = (Element) child;
		break;
	    }
	}

	if(localResponseString != null){
	    if(localResponseString.length() == 0){
		throw new MobyException("The MOBY payload has no text contents at all"); 
	    }
	    if(Character.isWhitespace(localResponseString.charAt(0))){
		localResponseString = localResponseString.trim();
	    }
	}

	// Check if the payload is an XML document.  If not, try a last ditch effort 
	// by base64 decoding the contents.  This is technically not allowable in the 
	// MOBY spec, but we are being lenient.
	if(localResponseString != null && !localResponseString.startsWith("<?xml")){
	    // Is the XML declaration missing?
	    if(localResponseString.startsWith("<moby:MOBY") || localResponseString.startsWith("<MOBY")){
		localResponseString = "<?xml version=\"1.0\"?>\n"+localResponseString;
		debugPS.println("Warning: The MOBY contents was missing an XML declaration, but it is " +
				"required by the MOBY API, and may stop working in the future without it.  Please " +
				"contact the client's provider to correct this.");
	    }
	    else{
		String oldResponse = localResponseString;
		localResponseString = new String(org.apache.axis.encoding.Base64.decode(localResponseString));
		if(!localResponseString.startsWith("<?xml")){
		    throw new MobyException("The SOAP payload defining the MOBY contents " +
					    "does not start with the xml processing instruction, and is therefore not " +
					    "an XML document, as specified in the MOBY API. " +
					    "Please contact the service provider.  Contents was: " + 
					    oldResponse);
		}
		debugPS.println("Warning: The MOBY contents was needlessly base64 encoded (the SOAP " +
				"envelope does this for you).  It has been decoded, but this is not " +
				"part of the MOBY API, and may stop working in the future.  Please " +
				"contact the service provider to correct this.");
	    }
	}

	// A bit of a hack: most MOBY data represented in string form
	// should have its formatting preserved (e.g. BLAST report), yet
	// no-one uses the xml:space="preserve" attribute.  This causes problems
	// later on because once the document is parsed, there is no way to get the 
	// spaces back!  I set the attribute explicitly at the top level of each data
	// element to compensate.  Unless of course this was already done.
	if(localResponseString != null && localResponseString.indexOf("xml:space=\"preserve\"") == -1){
	    localResponseString = localResponseString.replaceAll("<String", "<String xml:space=\"preserve\"");
	}

	try{
	    if(async && domRoot != null){
		// We will actually be appending several full MOBY messages together, so 
		// we need to do a DOM-meld at the mobyData tag level.
		Element newContentsTag = null;
		if(predefinedDOM != null){  // we have the MOBY element as a DOM Element already from the SOAP DOM
		    newContentsTag = MobyPrefixResolver.getChildElement(predefinedDOM, MobyTags.MOBYCONTENT);
		}
		else{
		    synchronized(docBuilder){
			Document newDoc = docBuilder.parse(new ByteArrayInputStream(localResponseString.getBytes()));
			newContentsTag = MobyPrefixResolver.getChildElement(newDoc.getDocumentElement(), MobyTags.MOBYCONTENT);
		    }
		}
		// Find the mobyContents tag under the root MOBY tag...
		Element existingContentsTag = MobyPrefixResolver.getChildElement(domRoot, MobyTags.MOBYCONTENT);
		NodeList newJobTags = newContentsTag.getChildNodes();
		for(int i = 0; newJobTags != null && i < newJobTags.getLength(); i++){
		    // Service notes blocks must be merged
		    if(newJobTags.item(i) instanceof Element &&
		       newJobTags.item(i).getLocalName().equals(MobyTags.SERVICENOTES)){
			Element existingServiceNotes = 
			    MobyPrefixResolver.getChildElement(existingContentsTag, MobyTags.SERVICENOTES);
			if(existingServiceNotes == null){
			    existingContentsTag.appendChild(newJobTags.item(i));
			}
			else{
			    NodeList newServiceData = newJobTags.item(i).getChildNodes();
			    for(int j = 0; newServiceData != null && j < newServiceData.getLength(); j++){
				existingServiceNotes.appendChild(newServiceData.item(j));
			    }
			}
		    }
		    else{  //everything else is at the same level (i.e. mobyData blocks)
			existingContentsTag.appendChild(newJobTags.item(i));
		    }
		}
	    }
	    else{  //synchronous service call, or first async call (which needs to create a doc anyway)
		// Synchronized to avoid Xerces exception FWK005: concurrent parsing is disallowed
		if(predefinedDOM != null){  // we have the MOBY element as a DOM Element already from the SOAP DOM
		    domRoot = predefinedDOM;
		}
		else{
		    synchronized(docBuilder){
			domRoot = docBuilder.parse(new ByteArrayInputStream(localResponseString.getBytes())).getDocumentElement();
		    }
		}      
	    }
	} catch(org.xml.sax.SAXException saxe){
	    throw new MobyException("The SOAP payload defining the MOBY Result " +
				    "could not be parsed: " + saxe);
	} catch(java.io.IOException ioe){
	    throw new MobyException("The SOAP payload defining the MOBY Result " +
				    " could not be read (from a String!)" + ioe);
	}

	// Now, either save the xml we got in the class instance member (for synchronous calls to MobyRequest)
	// or in the StringBuffer given as a parameter to this function (async calls)
	if(contentsXMLOutput != null){
	    contentsXMLOutput.append(localResponseString);
	}
	else{
	    responseString = localResponseString;
	}
	} // end for responseNode in responseNodes

	return domRoot;
    }

    public String convertMOBYDataToMOBYRequest(MobyDataInstance data) throws MobyException{
        return convertMOBYDataToMOBYRequest(new MobyContentInstance(data, ""));
    }

    /**
     * Creates an XML representation of the data, renamed to fit the needs of the service if necessary,
     * and adding any secondary parameter default values if not already specified in the incoming data.
     *
     * @param data the array of input parameters to put in a MOBY XML request
     *
     * @return the XML representation of the input data
     */
    public String convertMOBYDataToMOBYRequest(MobyContentInstance data) throws MobyException{

	MobyData[] inputs = mobyService.getPrimaryInputs();
	MobySecondaryData[] secondaries = mobyService.getSecondaryInputs();

	// Make sure the number of input args is correct for each query being submitted
	for(Map.Entry<String,MobyDataJob> entry: data.entrySet()){
	    String queryName = entry.getKey();
	    MobyDataJob query = entry.getValue();

	    // Additionally, we check if they are MobyDataInstances below
	    Map<String,MobyPrimaryData> primaryParams = new HashMap<String,MobyPrimaryData>();
	    Map<String,MobySecondaryData> secondaryParams = new HashMap<String,MobySecondaryData>();

	    // To store the primary input parameter name as given by the user, 
	    // in case we need it later on for parameter renaming...
	    String primaryParamName = null;  

	    for(Map.Entry<String,MobyDataInstance> subentry: query.entrySet()){
		String name = subentry.getKey();
		MobyDataInstance param = subentry.getValue();
		if(param == null){
		    throw new MobyException("Query " + queryName + 
					    " contained a null input parameter (" + name + ")");
		}
		else if(param instanceof MobyPrimaryData){
		    primaryParams.put(name, (MobyPrimaryData) param);
		    primaryParamName = name;
		}
		else if(param instanceof MobySecondaryData){
		    secondaryParams.put(name, (MobySecondaryData) param);
		}
		else{
		    System.err.println("Input parameter " + name + " (query " + queryName +
				       ") was not a MobyPrimaryData or MobySecondaryData " +
				       "as expected, but rather was of class " + param.getClass().getName());
		}
	    }

	    if(inputs != null && inputs.length != primaryParams.size()){
		throw new MobyException("Service " + mobyService.getName() + " was provided " + 
					primaryParams.size() + 
					" primary input parameter(s), but takes " + inputs.length + 
					" (query " + queryName + ")");
	    }
	    if(secondaries != null){
		// If no secondaries provided, fill them in by default
		if(secondaries.length != 0){
		    for(MobySecondaryData secondary: secondaries){
			if(!secondaryParams.containsKey(secondary.getName())){
			    if(debug){
				System.err.println("Setting default secondary param value for missing param " + secondary);
			    }
			    query.put(secondary.getName(), new MobyDataSecondaryInstance(secondary));
			}
		    }
		}
		if(secondaries.length != secondaryParams.size()){
		    throw new MobyException("Service " + mobyService.getName() + " was provided " + 
					    secondaryParams.size() + 
					    " secondary input parameter(s), but takes " + secondaries.length +
					    " (query " + queryName + ").  Extra secondary" +
					    " parameters must have been specified");
		}
	    }
	    
	    // If there was one anonymous input, assign the name automatically in
	    // the case the service requires it to be named.  This is the only
	    // unambiguous case in which we can do this.
	    if(inputs.length == 1){
		String serviceParamName = inputs[0].getName(); // name as req'd by the service

		// name isn't the same as required currently
		if(serviceParamName != null && serviceParamName.length() > 0 && 
		   !serviceParamName.equals(primaryParamName)){
		    // take out the old parameter
		    MobyPrimaryData theInputToRename = (MobyPrimaryData) query.remove(primaryParamName);

		    // Add in the same parameter, but with the appropriate name
		    query.put(serviceParamName, (MobyDataInstance) theInputToRename);
		}
	    }
	}

	ByteArrayOutputStream mobyRequest = new ByteArrayOutputStream();
	try{
	    MobyDataUtils.toXMLDocument(mobyRequest, data);
	}
	catch(MobyException me){
	    throw me;
	}
	catch(Exception e){
	    e.printStackTrace();
	    throw new MobyException("Could not create MOBY payload XML from input data: " +e);
	}

	logger.log(Level.FINE, "Input to MOBY Service is:\n"+mobyRequest.toString());
	
	return mobyRequest.toString();
    }

    /**
     * A method that sets up the execution environment for and runs a compiled XPath statement against a DOM node
     * @return the list of Nodes that satisfy the XPath in this Node's context
     */
    protected NodeList runXPath(XPathExpression xpath, Node n) throws XPathExpressionException{
	return (NodeList) xpath.evaluate(n, XPathConstants.NODESET);
    }
}
